Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 16 additions & 4 deletions agent-support/vscode/src/blame-lens-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1127,6 +1127,19 @@
* Extract model name from model string (e.g., "claude-3-opus-20240229" -> "Claude")
* Returns the part before the first "-" with first letter capitalized, or null if no model.
*/
private shouldHideModelName(model: string | undefined): boolean {
if (!model || model.trim() === '') {
return true;
}
const lower = model.trim().toLowerCase();
return (
lower === 'default' ||
lower === 'auto' ||
lower === 'unknown' ||
lower.endsWith('/unknown-model')
);
}

private extractModelName(modelString: string | undefined): string | null {
if (!modelString || modelString.trim() === '') {
return null;
Expand All @@ -1138,7 +1151,7 @@
if (trimmed === 'default' || trimmed === 'auto') {
return 'Cursor';
}
if (trimmed === 'unknown') {
if (trimmed === 'unknown' || trimmed.endsWith('/unknown-model')) {
return null; // Will display as "AI"
}

Expand Down Expand Up @@ -1281,9 +1294,8 @@
const tool = record?.agent_id?.tool || lineInfo.author;
const toolCapitalized = tool.charAt(0).toUpperCase() + tool.slice(1);

// Build model display: hide if default/auto/unknown/empty
const modelLower = model.toLowerCase();
const hideModel = !model || modelLower === 'default' || modelLower === 'auto' || modelLower === 'unknown';
// Build model display: hide placeholders and tool-scoped unknown fallbacks

Check warning on line 1297 in agent-support/vscode/src/blame-lens-manager.ts

View check run for this annotation

Autter.dev / autter/review-gate

🟠 Medium · Missing CODEOWNERS reviewer approval

`BlameLensManager.buildHoverContent`/`updateStatusBar` in `agent-support/vscode/src/blame-lens-manager.ts` changed, but there is no approving CODEOWNER review recorded for this path. Because `agent-support/vscode/src/extension.ts` depends on this utility, an unreviewed regression here would immediately alter the blame hover and status bar shown to users. Blast radius — skipping this guardrail cascades to the downstream usage that depends on this file: functions `BlameLensManager.updateStatusBar`, `BlameLensManager.rgbaToHex`, `BlameLensManager.getColorIndexForPromptId`, `BlameLensManager.buildHoverContent`, `BlameLensManager.updateAfterTextDecoration`, `BlameLensManager.extractModelName`, `BlameLensManager`, `CursorPreset`; scopes `autter-vscode`; dependent files `agent-support/vscode/src/extension.ts`, `vscode`, `agent-support/vscode/src/blame-service.ts`, `agent-support/vscode/src/utils/config.ts`, `agent-support/vscode/src/utils/git-api.ts`, `agent-support/vscode/src/utils/binary-path.ts`, `child_process`. **References:** - https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners — # About code owners You can use a CODEOWNERS file to define individuals or teams that are responsible for code in a repository. The people you choose as code owners must have write - https://docs.github.com/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners — # About code owners You can use a CODEOWNERS file to define individuals or teams that are responsible for code in a repository. The people you choose as code owners must have write Suggested fix: Request at least one approving review from the CODEOWNERS owner for `agent-support/vscode/src/blame-lens-manager.ts` before merging. Blast radius — skipping this guardrail cascades to the downstream usage that depends on this file: functions `BlameLensManager.updateStatusBar`, `BlameLensManager.rgbaToHex`, `BlameLensManager.getColorIndexForPromptId`, `BlameLensManager.buildHoverContent`, `BlameLensManager.updateAfterTextDecoration`, `BlameLensManager.extractModelName`, `BlameLensManager`, `CursorPreset`; scopes `autter-vscode`; dependent files `agent-support/vscode/src/extension.ts`, `vscode`, `agent-support/vscode/src/blame-service.ts`, `agent-support/vscode/src/utils/config.ts`, `agent-support/vscode/src/utils/git-api.ts`, `agent-support/vscode/src/utils/binary-path.ts`, `child_process`.

Check warning on line 1297 in agent-support/vscode/src/blame-lens-manager.ts

View check run for this annotation

Autter.dev / autter/review-gate

🟠 Medium · Missing CODEOWNERS reviewer approval

`BlameLensManager.buildHoverContent`/`updateStatusBar` in `agent-support/vscode/src/blame-lens-manager.ts` changed, but there is no approving CODEOWNER review recorded for this path. Because `agent-support/vscode/src/extension.ts` depends on this utility, an unreviewed regression here would immediately alter the blame hover and status bar shown to users. Blast radius — skipping this guardrail cascades to the downstream usage that depends on this file: functions `BlameLensManager.updateStatusBar`, `BlameLensManager.rgbaToHex`, `BlameLensManager.getColorIndexForPromptId`, `BlameLensManager.buildHoverContent`, `BlameLensManager.updateAfterTextDecoration`, `BlameLensManager.extractModelName`, `BlameLensManager`, `CursorPreset`; scopes `autter-vscode`; dependent files `agent-support/vscode/src/extension.ts`, `vscode`, `agent-support/vscode/src/blame-service.ts`, `agent-support/vscode/src/utils/config.ts`, `agent-support/vscode/src/utils/git-api.ts`, `agent-support/vscode/src/utils/binary-path.ts`, `child_process`. **References:** - https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners — # About code owners You can use a CODEOWNERS file to define individuals or teams that are responsible for code in a repository. The people you choose as code owners must have write - https://docs.github.com/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners — # About code owners You can use a CODEOWNERS file to define individuals or teams that are responsible for code in a repository. The people you choose as code owners must have write Suggested fix: Request at least one approving review from the CODEOWNERS owner for `agent-support/vscode/src/blame-lens-manager.ts` before merging. Blast radius — skipping this guardrail cascades to the downstream usage that depends on this file: functions `BlameLensManager.updateStatusBar`, `BlameLensManager.rgbaToHex`, `BlameLensManager.getColorIndexForPromptId`, `BlameLensManager.buildHoverContent`, `BlameLensManager.updateAfterTextDecoration`, `BlameLensManager.extractModelName`, `BlameLensManager`, `CursorPreset`; scopes `autter-vscode`; dependent files `agent-support/vscode/src/extension.ts`, `vscode`, `agent-support/vscode/src/blame-service.ts`, `agent-support/vscode/src/utils/config.ts`, `agent-support/vscode/src/utils/git-api.ts`, `agent-support/vscode/src/utils/binary-path.ts`, `child_process`.
const hideModel = this.shouldHideModelName(model);
const modelDisplay = hideModel ? '' : model;

// ═══════════════════════════════════════════════════════════════
Expand Down
166 changes: 159 additions & 7 deletions src/commands/checkpoint_agent/presets/cursor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
use crate::commands::checkpoint_agent::bash_tool::{self, Agent, ToolClass};
use crate::error::AutterError;
use std::collections::HashMap;
use std::path::PathBuf;
use std::path::{Path, PathBuf};

pub struct CursorPreset;

Expand Down Expand Up @@ -46,10 +46,11 @@

let hook_event_name = parse::required_str(&data, "hook_event_name")?;

// Extract model from hook input (Cursor provides this directly)
let model = parse::optional_str(&data, "model")
.unwrap_or("unknown")
.to_string();
let transcript_path = parse::optional_str(&data, "transcript_path").map(|s| s.to_string());

Check warning on line 49 in src/commands/checkpoint_agent/presets/cursor.rs

View check run for this annotation

Autter.dev / autter/review-gate

🟠 Medium · Missing CODEOWNERS reviewer approval

`CursorPreset::parse` and the new `resolve_cursor_model` helper changed the exported checkpoint path for `PreFileEdit`/`PostFileEdit`, but no approving CODEOWNER review is present for `src/commands/checkpoint_agent/presets/cursor.rs`. A bug here would cascade into persisted `AgentId.model` values and any consumer of checkpoint attribution. Blast radius — skipping this guardrail cascades to the downstream usage that depends on this file: functions `matching_workspace_root`, `cursor_file_path_from_tool_input`, `resolve_repo_cwd`, `parse`, `normalize_cursor_path`, `CursorPreset`, `BlameLensManager`; dependent files `agent-support/vscode/src/extension.ts`, `crate::authorship::authorship_log_serialization::generate_session_id`, `crate::authorship::working_log::AgentId`, `crate::commands::checkpoint_agent::bash_tool::{self, Agent, ToolClass}`, `crate::error::AutterError`, `std::collections::HashMap`, `std::path::PathBuf`, `super::parse`. **References:** - https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners — # About code owners You can use a CODEOWNERS file to define individuals or teams that are responsible for code in a repository. The people you choose as code owners must have write - https://docs.github.com/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners — # About code owners You can use a CODEOWNERS file to define individuals or teams that are responsible for code in a repository. The people you choose as code owners must have write Suggested fix: Get an approving review from the CODEOWNERS owner for `src/commands/checkpoint_agent/presets/cursor.rs` before merging. Blast radius — skipping this guardrail cascades to the downstream usage that depends on this file: functions `matching_workspace_root`, `cursor_file_path_from_tool_input`, `resolve_repo_cwd`, `parse`, `normalize_cursor_path`, `CursorPreset`, `BlameLensManager`; dependent files `agent-support/vscode/src/extension.ts`, `crate::authorship::authorship_log_serialization::generate_session_id`, `crate::authorship::working_log::AgentId`, `crate::commands::checkpoint_agent::bash_tool::{self, Agent, ToolClass}`, `crate::error::AutterError`, `std::collections::HashMap`, `std::path::PathBuf`, `super::parse`.

Check warning on line 49 in src/commands/checkpoint_agent/presets/cursor.rs

View check run for this annotation

Autter.dev / autter/review-gate

🟠 Medium · Missing CODEOWNERS reviewer approval

`CursorPreset::parse` and the new `resolve_cursor_model` helper changed the exported checkpoint path for `PreFileEdit`/`PostFileEdit`, but no approving CODEOWNER review is present for `src/commands/checkpoint_agent/presets/cursor.rs`. A bug here would cascade into persisted `AgentId.model` values and any consumer of checkpoint attribution. Blast radius — skipping this guardrail cascades to the downstream usage that depends on this file: functions `matching_workspace_root`, `cursor_file_path_from_tool_input`, `resolve_repo_cwd`, `parse`, `normalize_cursor_path`, `CursorPreset`, `BlameLensManager`; dependent files `agent-support/vscode/src/extension.ts`, `crate::authorship::authorship_log_serialization::generate_session_id`, `crate::authorship::working_log::AgentId`, `crate::commands::checkpoint_agent::bash_tool::{self, Agent, ToolClass}`, `crate::error::AutterError`, `std::collections::HashMap`, `std::path::PathBuf`, `super::parse`. **References:** - https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners — # About code owners You can use a CODEOWNERS file to define individuals or teams that are responsible for code in a repository. The people you choose as code owners must have write - https://docs.github.com/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners — # About code owners You can use a CODEOWNERS file to define individuals or teams that are responsible for code in a repository. The people you choose as code owners must have write Suggested fix: Get an approving review from the CODEOWNERS owner for `src/commands/checkpoint_agent/presets/cursor.rs` before merging. Blast radius — skipping this guardrail cascades to the downstream usage that depends on this file: functions `matching_workspace_root`, `cursor_file_path_from_tool_input`, `resolve_repo_cwd`, `parse`, `normalize_cursor_path`, `CursorPreset`, `BlameLensManager`; dependent files `agent-support/vscode/src/extension.ts`, `crate::authorship::authorship_log_serialization::generate_session_id`, `crate::authorship::working_log::AgentId`, `crate::commands::checkpoint_agent::bash_tool::{self, Agent, ToolClass}`, `crate::error::AutterError`, `std::collections::HashMap`, `std::path::PathBuf`, `super::parse`.

// Cursor documents both `model` and `model_id` on pre/postToolUse hooks.
// Fall back to the transcript when the hook only carries placeholders (Auto, etc.).
let model = resolve_cursor_model(&data, transcript_path.as_deref());

// Legacy hooks no longer installed; return error so orchestrator skips.
if hook_event_name == "beforeSubmitPrompt" || hook_event_name == "afterFileEdit" {
Expand Down Expand Up @@ -92,8 +93,6 @@
vec![]
};

let transcript_path = parse::optional_str(&data, "transcript_path").map(|s| s.to_string());

let mut metadata = HashMap::new();
if let Some(ref tp) = transcript_path {
metadata.insert("transcript_path".to_string(), tp.clone());
Expand Down Expand Up @@ -177,6 +176,45 @@
path.to_string()
}

fn is_cursor_placeholder_model(model: &str) -> bool {
let m = model.trim();
m.is_empty()
|| m.eq_ignore_ascii_case("unknown")
|| m.eq_ignore_ascii_case("default")
|| m.eq_ignore_ascii_case("auto")
}

/// Resolve the model for a Cursor hook: prefer hook `model`, then `model_id`, then transcript.
fn resolve_cursor_model(data: &serde_json::Value, transcript_path: Option<&str>) -> String {
let hook_model = parse::optional_str(data, "model");
let hook_model_id = parse::optional_str(data, "model_id");

if let Some(model) = hook_model {
if !is_cursor_placeholder_model(model) {
return model.to_string();
}
}

if let Some(model_id) = hook_model_id {
if !is_cursor_placeholder_model(model_id) {
return model_id.to_string();
}
}

if let Some(path) = transcript_path {
if let Ok(Some(model)) = crate::streams::model_extraction::extract_model(
Path::new(path),
crate::streams::sweep::StreamFormat::CursorJsonl,
None,
) && !is_cursor_placeholder_model(&model)
{
return model;
}
}

"unknown".to_string()
}

fn cursor_file_path_from_tool_input(tool_input: Option<&serde_json::Value>) -> String {
tool_input
.and_then(|ti| {
Expand Down Expand Up @@ -512,6 +550,120 @@
}
}

#[test]
fn test_cursor_model_id_fallback_when_model_missing() {
let input = json!({
"conversation_id": "conv-123",
"workspace_roots": ["/home/user/project"],
"hook_event_name": "preToolUse",
"tool_name": "Write",
"model_id": "claude-opus-4-7",
"tool_input": {"file_path": "src/main.rs"}
})
.to_string();
let events = CursorPreset.parse(&input, "t_test123456789a").unwrap();
match &events[0] {
ParsedHookEvent::PreFileEdit(e) => {
assert_eq!(e.context.agent_id.model, "claude-opus-4-7");
}
_ => panic!("Expected PreFileEdit"),
}
}

#[test]
fn test_cursor_prefers_model_over_model_id() {
let input = json!({
"conversation_id": "conv-123",
"workspace_roots": ["/home/user/project"],
"hook_event_name": "preToolUse",
"tool_name": "Write",
"model": "composer-2",
"model_id": "claude-opus-4-7",
"tool_input": {"file_path": "src/main.rs"}
})
.to_string();
let events = CursorPreset.parse(&input, "t_test123456789a").unwrap();
match &events[0] {
ParsedHookEvent::PreFileEdit(e) => {
assert_eq!(e.context.agent_id.model, "composer-2");
}
_ => panic!("Expected PreFileEdit"),
}
}

#[test]
fn test_cursor_model_id_fallback_when_model_is_placeholder() {
let input = json!({
"conversation_id": "conv-123",
"workspace_roots": ["/home/user/project"],
"hook_event_name": "preToolUse",
"tool_name": "Write",
"model": "auto",
"model_id": "claude-opus-4-7",
"tool_input": {"file_path": "src/main.rs"}
})
.to_string();
let events = CursorPreset.parse(&input, "t_test123456789a").unwrap();
match &events[0] {
ParsedHookEvent::PreFileEdit(e) => {
assert_eq!(e.context.agent_id.model, "claude-opus-4-7");
}
_ => panic!("Expected PreFileEdit"),
}
}

#[test]
fn test_cursor_unknown_when_only_placeholder_model_and_no_transcript() {
let input = json!({
"conversation_id": "conv-123",
"workspace_roots": ["/home/user/project"],
"hook_event_name": "preToolUse",
"tool_name": "Write",
"model": "auto",
"tool_input": {"file_path": "src/main.rs"}
})
.to_string();
let events = CursorPreset.parse(&input, "t_test123456789a").unwrap();
match &events[0] {
ParsedHookEvent::PreFileEdit(e) => {
assert_eq!(e.context.agent_id.model, "unknown");
}
_ => panic!("Expected PreFileEdit"),
}
}

#[test]
fn test_cursor_resolves_model_from_transcript_when_hook_is_auto() {
use std::io::Write;
use tempfile::NamedTempFile;

let mut transcript = NamedTempFile::new().unwrap();
writeln!(
transcript,
r#"{{"role":"assistant","message":{{"model":"claude-sonnet-4","content":[{{"type":"text","text":"hi"}}]}}}}"#
)
.unwrap();
transcript.flush().unwrap();

let input = json!({
"conversation_id": "conv-123",
"workspace_roots": ["/home/user/project"],
"hook_event_name": "postToolUse",
"tool_name": "Write",
"model": "auto",
"transcript_path": transcript.path().to_string_lossy(),
"tool_input": {"file_path": "src/main.rs"}
})
.to_string();
let events = CursorPreset.parse(&input, "t_test123456789a").unwrap();
match &events[0] {
ParsedHookEvent::PostFileEdit(e) => {
assert_eq!(e.context.agent_id.model, "claude-sonnet-4");
}
_ => panic!("Expected PostFileEdit"),
}
}

#[test]
fn test_matching_workspace_root() {
let roots = vec![
Expand Down
23 changes: 21 additions & 2 deletions src/streams/model_extraction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
match format {
StreamFormat::ClaudeJsonl
| StreamFormat::CopilotEventStreamJsonl
| StreamFormat::GeminiJsonl => extract_model_from_jsonl_tail(path),
| StreamFormat::GeminiJsonl

Check warning on line 15 in src/streams/model_extraction.rs

View check run for this annotation

Autter.dev / autter/review-gate

🟠 Medium · Missing CODEOWNERS reviewer approval

`extract_model` now accepts `StreamFormat::CursorJsonl` and reads top-level `model_id` values, but there is no approving CODEOWNER review on `src/streams/model_extraction.rs`. That parser is now part of the fallback chain used by `CursorPreset::resolve_cursor_model`, so a mistake would silently affect transcript-derived attribution across cursor hooks. Blast radius — skipping this guardrail cascades to the downstream usage that depends on this file: functions `extract_model_from_jsonl_line`, `extract_model_from_jsonl_head`, `extract_model_from_jsonl_tail`, `test_extract_model_empty_file`, `test_extract_model_missing_file`, `test_extract_model_droid_settings_missing_file`, `BlameLensManager`, `CursorPreset`; dependent files `agent-support/vscode/src/extension.ts`, `crate::streams::sweep::StreamFormat`, `crate::streams::types::StreamError`, `std::fs::File`, `std::io::{BufRead, BufReader, Seek, SeekFrom}`, `std::path::Path`. **References:** - https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners — # About code owners You can use a CODEOWNERS file to define individuals or teams that are responsible for code in a repository. The people you choose as code owners must have write - https://docs.github.com/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners — # About code owners You can use a CODEOWNERS file to define individuals or teams that are responsible for code in a repository. The people you choose as code owners must have write Suggested fix: Request an approving review from the CODEOWNERS owner for `src/streams/model_extraction.rs`. Blast radius — skipping this guardrail cascades to the downstream usage that depends on this file: functions `extract_model_from_jsonl_line`, `extract_model_from_jsonl_head`, `extract_model_from_jsonl_tail`, `test_extract_model_empty_file`, `test_extract_model_missing_file`, `test_extract_model_droid_settings_missing_file`, `BlameLensManager`, `CursorPreset`; dependent files `agent-support/vscode/src/extension.ts`, `crate::streams::sweep::StreamFormat`, `crate::streams::types::StreamError`, `std::fs::File`, `std::io::{BufRead, BufReader, Seek, SeekFrom}`, `std::path::Path`.

Check warning on line 15 in src/streams/model_extraction.rs

View check run for this annotation

Autter.dev / autter/review-gate

🟠 Medium · Missing CODEOWNERS reviewer approval

`extract_model` now accepts `StreamFormat::CursorJsonl` and reads top-level `model_id` values, but there is no approving CODEOWNER review on `src/streams/model_extraction.rs`. That parser is now part of the fallback chain used by `CursorPreset::resolve_cursor_model`, so a mistake would silently affect transcript-derived attribution across cursor hooks. Blast radius — skipping this guardrail cascades to the downstream usage that depends on this file: functions `extract_model_from_jsonl_line`, `extract_model_from_jsonl_head`, `extract_model_from_jsonl_tail`, `test_extract_model_empty_file`, `test_extract_model_missing_file`, `test_extract_model_droid_settings_missing_file`, `BlameLensManager`, `CursorPreset`; dependent files `agent-support/vscode/src/extension.ts`, `crate::streams::sweep::StreamFormat`, `crate::streams::types::StreamError`, `std::fs::File`, `std::io::{BufRead, BufReader, Seek, SeekFrom}`, `std::path::Path`. **References:** - https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners — # About code owners You can use a CODEOWNERS file to define individuals or teams that are responsible for code in a repository. The people you choose as code owners must have write - https://docs.github.com/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners — # About code owners You can use a CODEOWNERS file to define individuals or teams that are responsible for code in a repository. The people you choose as code owners must have write Suggested fix: Request an approving review from the CODEOWNERS owner for `src/streams/model_extraction.rs`. Blast radius — skipping this guardrail cascades to the downstream usage that depends on this file: functions `extract_model_from_jsonl_line`, `extract_model_from_jsonl_head`, `extract_model_from_jsonl_tail`, `test_extract_model_empty_file`, `test_extract_model_missing_file`, `test_extract_model_droid_settings_missing_file`, `BlameLensManager`, `CursorPreset`; dependent files `agent-support/vscode/src/extension.ts`, `crate::streams::sweep::StreamFormat`, `crate::streams::types::StreamError`, `std::fs::File`, `std::io::{BufRead, BufReader, Seek, SeekFrom}`, `std::path::Path`.
| StreamFormat::CursorJsonl => extract_model_from_jsonl_tail(path),
StreamFormat::CopilotSessionJson => extract_model_from_copilot_session_json(path),
StreamFormat::AmpThreadJson => extract_model_from_amp_thread_json(path),
StreamFormat::OpenCodeSqlite => extract_model_from_opencode_sqlite(path, session_id),
Expand Down Expand Up @@ -103,7 +104,8 @@
.get("message")
.and_then(|m| m.get("model"))
.and_then(|v| v.as_str())
.or_else(|| json.get("model").and_then(|v| v.as_str()));
.or_else(|| json.get("model").and_then(|v| v.as_str()))

Check failure on line 107 in src/streams/model_extraction.rs

View check run for this annotation

Autter.dev / autter/review-gate

🔴 High · Do not let a placeholder transcript model hide its concrete model_id

The Cursor resolver invokes this generic extractor only after both hook fields are unusable. For a Cursor transcript record containing `model: "auto"` and `model_id: "claude-opus-4-7"`, this `or_else` chain selects the present placeholder at line 107, never evaluates `model_id` at line 108, and returns `auto`. `resolve_cursor_model` then rejects that placeholder (cursor.rs:209) and returns `unknown` (cursor.rs:215), despite the transcript carrying a concrete model identity. This is reachable on postToolUse when Cursor's hook reports Auto/default and the transcript is the fallback source, so persisted attribution is incorrectly normalized to `cursor/unknown-model` rather than the actual model. Suggested fix: When extracting a Cursor JSONL record, resolve `model` and `model_id` independently: use a non-placeholder `model`; otherwise use a non-placeholder `model_id`. Keep the existing precedence for non-Cursor formats or make placeholder filtering format-aware. Add coverage for a Cursor transcript line with `model: "auto"` and a concrete `model_id`.

Check failure on line 107 in src/streams/model_extraction.rs

View check run for this annotation

Autter.dev / autter/review-gate

🔴 High · Do not let a placeholder transcript model mask model_id

When the hook only supplies a placeholder, `resolve_cursor_model` intentionally falls back to `extract_model`; however, JSONL extraction chooses a present top-level `model` before `model_id` without applying the placeholder check. A Cursor record containing `model: "auto"` and `model_id: "claude-opus-4-7"` therefore returns `auto`; the resolver rejects it, silently treats transcript extraction as unsuccessful, and returns `unknown`. The orchestrator then normalizes that value to `cursor/unknown-model` before the checkpoint is persisted. Thus a transient/pre-tool hook with no usable hook model loses the real transcript model rather than producing an error or retaining the available identity. Suggested fix: Make Cursor JSONL extraction resolve `model` and `model_id` independently, preferring a non-placeholder `model`, then a non-placeholder `model_id`. Ensure the resolver can still fall back to `unknown` only when neither transcript field is usable, and add coverage for a Cursor JSONL record with `model: auto` plus a concrete `model_id`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟠 [ai] Do not let a placeholder transcript model hide its concrete model_id — Risk: 78/100

The Cursor resolver invokes this generic extractor only after both hook fields are unusable. For a Cursor transcript record containing model: "auto" and model_id: "claude-opus-4-7", this or_else chain selects the present placeholder at line 107, never evaluates model_id at line 108, and returns auto. resolve_cursor_model then rejects that placeholder (cursor.rs:209) and returns unknown (cursor.rs:215), despite the transcript carrying a concrete model identity. This is reachable on postToolUse when Cursor's hook reports Auto/default and the transcript is the fallback source, so persisted attribution is incorrectly normalized to cursor/unknown-model rather than the actual model.

⚠ Downstream affected — if this fails, it cascades to the usage that depends on this file:

  • Dependent files: src/commands/checkpoint_agent/presets/cursor.rs, src/streams/model_extraction.rs, src/authorship/working_log.rs
🛠 AI fix prompt (copy & paste into your coding agent)
When extracting a Cursor JSONL record, resolve `model` and `model_id` independently: use a non-placeholder `model`; otherwise use a non-placeholder `model_id`. Keep the existing precedence for non-Cursor formats or make placeholder filtering format-aware. Add coverage for a Cursor transcript line with `model: "auto"` and a concrete `model_id`.

Flagged by Autter security & observability checks.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟠 [ai] Do not let a placeholder transcript model mask model_id — Risk: 74/100

When the hook only supplies a placeholder, resolve_cursor_model intentionally falls back to extract_model; however, JSONL extraction chooses a present top-level model before model_id without applying the placeholder check. A Cursor record containing model: "auto" and model_id: "claude-opus-4-7" therefore returns auto; the resolver rejects it, silently treats transcript extraction as unsuccessful, and returns unknown. The orchestrator then normalizes that value to cursor/unknown-model before the checkpoint is persisted. Thus a transient/pre-tool hook with no usable hook model loses the real transcript model rather than producing an error or retaining the available identity.

⚠ Downstream affected — if this fails, it cascades to the usage that depends on this file:

  • Dependent files: src/streams/model_extraction.rs, src/commands/checkpoint_agent/presets/cursor.rs, src/commands/checkpoint_agent/orchestrator.rs, src/authorship/working_log.rs
🛠 AI fix prompt (copy & paste into your coding agent)
Make Cursor JSONL extraction resolve `model` and `model_id` independently, preferring a non-placeholder `model`, then a non-placeholder `model_id`. Ensure the resolver can still fall back to `unknown` only when neither transcript field is usable, and add coverage for a Cursor JSONL record with `model: auto` plus a concrete `model_id`.

Flagged by Autter security & observability checks.

.or_else(|| json.get("model_id").and_then(|v| v.as_str()));

Check failure on line 108 in src/streams/model_extraction.rs

View check run for this annotation

Autter.dev / autter/review-gate

🔴 High · Cursor transcript fallback lets a placeholder `model` hide a concrete `model_id`

`resolve_cursor_model` delegates placeholder hook inputs to `extract_model` and only accepts a non-placeholder return. The shared JSONL extractor selects `message.model` first, then top-level `model`, and only then `model_id`. Consequently a valid Cursor transcript record containing `model: "auto"` (or `message.model: "auto"`) alongside `model_id: "claude-opus-4-7"` returns `auto`; the resolver rejects it and persists `unknown`, never considering the concrete ID. This breaks the checkpoint orchestrator's new Cursor model-resolution contract for pre/post hook events that must rely on transcript fallback, losing the model identity in the `AgentId` sent to downstream checkpoint normalization and authorship logging. Suggested fix: When extracting a CursorJsonl model, evaluate `model` and `model_id` independently and prefer a non-placeholder value. Either make `extract_model` format-aware or have `resolve_cursor_model` inspect Cursor transcript records with Cursor placeholder rules, so `auto`/`default`/`unknown` cannot mask a concrete `model_id`. Add coverage for a transcript-only fallback record with `model: auto` plus `model_id: claude-opus-4-7`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟠 [ai] Cursor transcript fallback lets a placeholder model hide a concrete model_id — Risk: 78/100

resolve_cursor_model delegates placeholder hook inputs to extract_model and only accepts a non-placeholder return. The shared JSONL extractor selects message.model first, then top-level model, and only then model_id. Consequently a valid Cursor transcript record containing model: "auto" (or message.model: "auto") alongside model_id: "claude-opus-4-7" returns auto; the resolver rejects it and persists unknown, never considering the concrete ID. This breaks the checkpoint orchestrator's new Cursor model-resolution contract for pre/post hook events that must rely on transcript fallback, losing the model identity in the AgentId sent to downstream checkpoint normalization and authorship logging.

⚠ Downstream affected — if this fails, it cascades to the usage that depends on this file:

  • Dependent files: src/commands/checkpoint_agent/orchestrator.rs, src/commands/checkpoint_agent/presets/cursor.rs, src/streams/model_extraction.rs
🛠 AI fix prompt (copy & paste into your coding agent)
When extracting a CursorJsonl model, evaluate `model` and `model_id` independently and prefer a non-placeholder value. Either make `extract_model` format-aware or have `resolve_cursor_model` inspect Cursor transcript records with Cursor placeholder rules, so `auto`/`default`/`unknown` cannot mask a concrete `model_id`. Add coverage for a transcript-only fallback record with `model: auto` plus `model_id: claude-opus-4-7`.

Flagged by Autter security & observability checks.


if let Some(model) = candidate
&& model != "<synthetic>"
Expand Down Expand Up @@ -369,6 +371,23 @@
assert_eq!(result, None);
}

#[test]

Check warning on line 374 in src/streams/model_extraction.rs

View check run for this annotation

Autter.dev / autter/review-gate

🟠 Medium · Missing test coverage for top-level Cursor `model_id` extraction

The added Cursor JSONL test only asserts extraction from `message.model`. Add a case containing a top-level `model_id` and no nested model field, and assert that `extract_model(..., StreamFormat::CursorJsonl, ...)` returns that value. Suggested fix: Add a sibling test covering a Cursor JSONL record whose usable model appears only in top-level `model` or `model_id`, and assert `extract_model(..., StreamFormat::CursorJsonl,...)` returns it. Blast radius — skipping this guardrail cascades to the downstream usage that depends on this file: functions `extract_model_from_jsonl_line`, `extract_model_from_jsonl_head`, `extract_model_from_jsonl_tail`, `test_extract_model_empty_file`, `test_extract_model_missing_file`, `test_extract_model_droid_settings_missing_file`, `BlameLensManager`, `CursorPreset`; dependent files `agent-support/vscode/src/extension.ts`, `crate::streams::sweep::StreamFormat`, `crate::streams::types::StreamError`, `std::fs::File`, `std::io::{BufRead, BufReader, Seek, SeekFrom}`, `std::path::Path`.

Check warning on line 374 in src/streams/model_extraction.rs

View check run for this annotation

Autter.dev / autter/review-gate

🟠 Medium · Missing test coverage for top-level Cursor `model_id` extraction

The added Cursor JSONL test only asserts extraction from `message.model`. Add a case containing a top-level `model_id` and no nested model field, and assert that `extract_model(..., StreamFormat::CursorJsonl, ...)` returns that value. Suggested fix: Add a sibling test covering a Cursor JSONL record whose usable model appears only in top-level `model` or `model_id`, and assert `extract_model(..., StreamFormat::CursorJsonl,...)` returns it. Blast radius — skipping this guardrail cascades to the downstream usage that depends on this file: functions `extract_model_from_jsonl_line`, `extract_model_from_jsonl_head`, `extract_model_from_jsonl_tail`, `test_extract_model_empty_file`, `test_extract_model_missing_file`, `test_extract_model_droid_settings_missing_file`, `BlameLensManager`, `CursorPreset`; dependent files `agent-support/vscode/src/extension.ts`, `crate::streams::sweep::StreamFormat`, `crate::streams::types::StreamError`, `std::fs::File`, `std::io::{BufRead, BufReader, Seek, SeekFrom}`, `std::path::Path`.
fn test_extract_model_cursor_jsonl() {
use std::io::Write;
use tempfile::NamedTempFile;

let mut file = NamedTempFile::new().unwrap();
writeln!(
file,
r#"{{"role":"assistant","message":{{"model":"composer-2","content":[{{"type":"text","text":"ok"}}]}}}}"#
)
.unwrap();
file.flush().unwrap();

let result = extract_model(file.path(), StreamFormat::CursorJsonl, None).unwrap();
assert_eq!(result, Some("composer-2".to_string()));
}

#[test]
fn test_extract_model_missing_file() {
let path = PathBuf::from("/nonexistent/path/to/file.jsonl");
Expand Down
Loading