Repository navigation
fix(parsers): fill a blank Cursor model from the assistant message - #1104
gongcavalry wants to merge 6 commits into
Conversation
…gerprint Cursor sessions imported from disk often carry no model name. Recover it by walking the session's composer/bubble fingerprint, and remember the result so later turns of the same session keep the attribution. Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
🟡 Changes recommended
The model backfill can persist incorrect session models, reinsert hidden conversations into client state, and delay search results and turn-completion updates with synchronous store scans.
5 open issues
What changed in this PR
This PR recovers missing model names from Cursor session stores so Codeg can show model attribution in conversation lists and details.
Changes:
- Resolve models from fingerprints reachable from the latest session root, while preferring explicit model metadata.
- Persist recovered models during conversation listing and turn completion, broadcasting updates to clients.
- Add a parser regression test that distinguishes linked fingerprints from stale, unlinked ones.
| File | Description |
|---|---|
src-tauri/src/parsers/cursor.rs |
Add fingerprint-based model recovery and a regression test. |
src-tauri/src/db/service/conversation_service.rs |
Query Cursor conversations with missing models. |
src-tauri/src/commands/conversations.rs |
Backfill and persist recovered models, then broadcast updates. |
src-tauri/src/acp/lifecycle.rs |
Trigger model recovery when a Cursor turn completes. |
Review effort: Max
| // Cursor does not put the model on the conversation row. The | ||
| // store has it once the turn is written; copy it onto the chip | ||
| // before the status broadcast so this same update shows the letter. | ||
| crate::commands::conversations::remember_cursor_session_model( | ||
| db_conn, | ||
| &emitter, | ||
| cid, | ||
| agent_type, | ||
| external_id.as_deref(), | ||
| ) | ||
| .await; | ||
| if claimed { | ||
| write_turn_status(db_conn, &state_arc, &emitter, cid, stop_reason).await?; | ||
| } |
There was a problem hiding this comment.
Fixed in 72703f7. The turn status is now written and broadcast BEFORE the optional model lookup, and the model arrives through its own upsert right after — a Cursor row with no model no longer delays the turn-completion event.
| // current stores have no `lastUsedModel`. Fill the blank chip from the | ||
| // fingerprint still linked in that session, then list. Once a row has a | ||
| // model this finds nothing and does not open the stores again. | ||
| if let Err(error) = backfill_cursor_models(conn, emitter).await { |
There was a problem hiding this comment.
Fixed in 72703f7. The backfill no longer runs on the list request's critical path: it is scheduled as a single-flight background task, and unsuccessful lookups are negative-cached by store mtime — a session whose store yields no model is not walked again until the store file actually changes.
| .with_detail(error.to_string()) | ||
| })?; | ||
| for (id, model) in found { | ||
| if conversation_service::seed_model_if_empty(conn, id, &model) |
There was a problem hiding this comment.
Fixed in 72703f7 (both sites). The seed is now seed_model_if_empty_for_session: one UPDATE that also matches the external session id and the Cursor agent type, keeping the deletion and empty-model guards, so a row rebound during the blocking lookup turns the write into a no-op instead of inheriting the previous session's model.
| let rows = conversation::Entity::find() | ||
| .filter(conversation::Column::DeletedAt.is_null()) | ||
| .filter(conversation::Column::AgentType.eq(AgentType::Cursor.as_wire().into_owned())) | ||
| .filter(conversation::Column::ExternalId.is_not_null()) |
There was a problem hiding this comment.
Fixed in 72703f7. cursor_sessions_missing_model now applies the same live-folder and non-loop filters as refresh_codex_auto_titles, so a successful backfill can no longer broadcast an upsert that reinserts a hidden row into another client's workspace state.
| i += 1; | ||
| let start = i; | ||
| while window.get(i).is_some_and(|byte| *byte != b'"' && *byte != b'\\') { | ||
| i += 1; | ||
| } | ||
| let id = std::str::from_utf8(window.get(start..i)?).ok()?; | ||
| (!id.is_empty() && id.len() <= 100).then(|| id.to_string()) |
There was a problem hiding this comment.
Fixed in 72703f7. The value is now decoded as a complete JSON string (decode_json_string): escapes (\", \\, \/, \uXXXX, …) are fully decoded, and anything incomplete — a backslash or the end of the window before the closing quote, a malformed escape, a lone surrogate — is rejected, so a truncated id can never be persisted. Unit tests cover the escape and rejection cases.
… decode - lifecycle: broadcast the turn status BEFORE the optional Cursor model lookup; the status event carries no model and must not wait on a store walk of up to 4,000 blobs. - list: run the model backfill in a single-flight background task instead of on the list request's critical path (search hits this endpoint after every 300 ms debounce), and negative-cache unresolved sessions until their store file changes. - Seed the model with an UPDATE that also matches the external session id and Cursor agent type, so a row rebound during the blocking lookup never inherits the previous session's model. - Exclude soft-deleted folders and loop conversations from the missing- model query, mirroring refresh_codex_auto_titles, so backfill upserts cannot reinsert hidden rows into client state. - Decode the fingerprint's "model" as a complete JSON string (escapes included) and reject incomplete values — a backslash or the end of the scan window no longer yields a truncated model id. Co-authored-by: Cursor <cursoragent@cursor.com>
Copilot 评审后的原子标题守卫和 Cursor 模型回补已在 PR 分支上 CI 全绿,本机日常包仍是评审前版本。三路合并进 bc,重启后与已回捐的修复一致。 Co-authored-by: Cursor <cursoragent@cursor.com>
|
@gongcavalry thanks a lot for this, and for turning around the first review so quickly! The blank model chip on Cursor rows is a real gap. I confirmed it locally: none of the Cursor ACP stores on my machine has
My conclusion, though, is that the source of the model needs to change before this can merge. Changing only the marker would not be enough. I've tried to put everything in this one comment so you have the full picture in one place. Summary
§1 The fingerprint marker isn't in the stores I can inspect
So on every store I can inspect, the new fallback returns The DFS order also doesn't tell us which turn a fingerprint belongs to. The walk keeps whichever hit comes last in traversal order, which isn't necessarily the latest turn. §2 Where the model actually livesIn the ACP stores I inspected, each assistant message is a JSON blob, and the model sits in a structured field at "providerOptions": {"cursor": {"modelName": "composer-2.5"}}My stores have Reading this field with How I checkedCopy each sqlite3 s.db "select data from blobs" | LC_ALL=C grep -aoE '.{0,30}[Mm]odel.{0,80}' | sort -u
sqlite3 s.db "select count(*) from blobs where instr(lower(cast(data as text)), 'fingerprint') > 0" # 0 in all fiveFor the CLI builds, I extracted §3 Sessions run in codeg: the model is already in memoryWhen a turn ends, the lifecycle handler already holds the session's
Also, once the parser can name a model, the existing path saves it on its own. After a turn, the panel's §4 Suggested shape
§5 Smaller notes, tests, and a question
I'd suggest reworking along these lines before we merge. If I've misread anything, or your stores really do look different, please push back, and a store sample would settle it quickly. Thanks again for picking this up! |
|
codeg work task |
The fingerprint walk does not match the stores this review can inspect, and its order is not the turn order. Live turns already have the selected model in memory. History uses the latest assistant modelName, and only an empty chip is filled. Co-authored-by: Cursor <cursoragent@cursor.com>
The WAL retry test rewrote store.db before touching the wal, so a cache that ignored mtime would still pass. Assert the chip stays empty until the wal mtime changes. Co-authored-by: Cursor <cursoragent@cursor.com>
|
Reworked the model source along the lines in the review. The fingerprint walk is gone, including Live turns. The lifecycle handler copies History. Backfill. Same parser, still single-flight, still filtered, still the atomic seed. The negative cache uses the newer of On the fingerprint question. A redacted root, with ids and message text removed:
{
"role": "assistant",
"content": [
{
"type": "reasoning",
"providerOptions": {
"cursor": { "modelName": "accounts/fireworks/models/kimi-k3" }
}
},
{ "type": "text" },
{ "type": "tool-call" }
]
}
Tests use temp directories only ( |
…t first - Turn end now reads the model this turn recorded in the store and falls back to the selector only when the store names none. In the parameterized picker codeg asks for, Cursor's selector holds the base model (`grok-4.7`) while each reply records the variant that ran (`grok-4.7-high`), so a live row and a backfilled row used to differ. Cursor writes the reply's blobs before it answers the prompt, so the store already has this turn. A row that already has a model skips the lookup. - The assistant-model lookup checks the inline field-4 message, then the field-1 refs newest first, and stops at the first model. That gives the same answer as reading every message, but usually reads one or two blobs. - Reword the ref cap's doc comment, which still mentioned the fingerprint walk.
…ides The Cursor model seed now also requires a live folder and a non-loop row in the same UPDATE. That's the filter the backfill already selects with. Both callers broadcast an upsert when the seed writes, and clients insert any upserted row they don't have. Before this, a folder soft-deleted while the store was read, or a loop conversation finishing a Cursor turn, could come back into another client's workspace list. The backfill's single-flight flag is now cleared by a drop guard, so an error, a panic or a dropped task can't leave it set and stop every later pass.
|
@gongcavalry thank you for the rework, and for the detailed numbers from your 65 stores! They settled the fingerprint question better than anything I could check here. The new version follows the plan closely. It reads the structured I pushed two commits on top of your branch so we don't need another round. Here is what they change and why. 1. Turn end now reads the store first (
|

Problem
Cursor ACP stores on current builds do not put
lastUsedModelin the session metadata, so the sidebar chip stays blank.Fix
config_options) after the turn status is written. This path does not open the store.content[].providerOptions.cursor.modelNamefrom assistant messages: root field 1 in order, then an inline field 4. The latest non-empty name wins. The fingerprint byte walk is removed.store.dbandstore.db-wal.Tests
Temp directories only. These tests do not read
~/.cursor.modelName, a model switch, inline field 4, andtext/tool-callentries that omitmodelNamestore.dbis rewritten but its mtime is restored, then retries after onlystore.db-walchangesconfig_optionsand does not overwrite a chip that is already filled