Repository navigation
feat(parsers): backfill a Cursor session's missing model from its fingerprint - #1104
gongcavalry wants to merge 2 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>

Problem
Sessions imported from Cursor's on-disk state often carry no model name, so the sidebar/detail views show nothing for conversations that were in fact run with a specific model.
Fix
parsers/cursor.rs: walk the session's composer/bubble fingerprint to recover the model that produced the turns (cursor_sessions_missing_modelcovers the no-model import path).acp/lifecycle.rskeeps the recovered value across the session lifecycle.Tests
cargo test --lib cursor(109) andcargo test --lib missing_modelpass.Made with Cursor