Skip to content

feat(parsers): backfill a Cursor session's missing model from its fingerprint - #1104

Open
gongcavalry wants to merge 2 commits into
spacering-net:mainfrom
gongcavalry:pr/cursor-model-backfill
Open

gongcavalry wants to merge 2 commits into
spacering-net:mainfrom
gongcavalry:pr/cursor-model-backfill

Conversation

@gongcavalry

Copy link
Copy Markdown
Contributor

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_model covers the no-model import path).
  • Remember the recovered model so later turns of the same session keep the attribution instead of re-walking the fingerprint.
  • acp/lifecycle.rs keeps the recovered value across the session lifecycle.

Tests

  • New tests for the missing-model import path and the remember/backfill flow.
  • cargo test --lib cursor (109) and cargo test --lib missing_model pass.

Made with Cursor

…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>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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

Comment thread src-tauri/src/acp/lifecycle.rs Outdated
Comment on lines 356 to 369
// 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?;
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src-tauri/src/commands/conversations.rs Outdated
// 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 {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src-tauri/src/commands/conversations.rs Outdated
.with_detail(error.to_string())
})?;
for (id, model) in found {
if conversation_service::seed_model_if_empty(conn, id, &model)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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())

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src-tauri/src/parsers/cursor.rs Outdated
Comment on lines +551 to +557
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())

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
gongcavalry added a commit to gongcavalry/codeg that referenced this pull request Nov 7, 2026
Copilot 评审后的原子标题守卫和 Cursor 模型回补已在 PR 分支上 CI 全绿,本机日常包仍是评审前版本。三路合并进 bc,重启后与已回捐的修复一致。

Co-authored-by: Cursor <cursoragent@cursor.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants