diff --git a/cli/migrations/agent-trace/015_create_session_models.sql b/cli/migrations/agent-trace/015_create_session_models.sql index 4f909c80..961f265f 100644 --- a/cli/migrations/agent-trace/015_create_session_models.sql +++ b/cli/migrations/agent-trace/015_create_session_models.sql @@ -2,7 +2,7 @@ CREATE TABLE IF NOT EXISTS session_models ( id INTEGER PRIMARY KEY, tool_name TEXT NOT NULL, session_id TEXT NOT NULL, - model_id TEXT NOT NULL, + model_id TEXT, tool_version TEXT, session_start_time_ms INTEGER NOT NULL, created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')), diff --git a/cli/src/services/agent_trace_db/mod.rs b/cli/src/services/agent_trace_db/mod.rs index ecd38f7a..bef3dd6f 100644 --- a/cli/src/services/agent_trace_db/mod.rs +++ b/cli/src/services/agent_trace_db/mod.rs @@ -149,7 +149,7 @@ pub struct DiffTraceInsert<'a> { pub struct SessionModelUpsert<'a> { pub tool_name: &'a str, pub session_id: &'a str, - pub model_id: &'a str, + pub model_id: Option<&'a str>, pub tool_version: Option<&'a str>, pub session_start_time_ms: i64, } @@ -159,7 +159,7 @@ pub struct SessionModelUpsert<'a> { pub struct SessionModelAttribution { pub tool_name: String, pub session_id: String, - pub model_id: String, + pub model_id: Option, pub tool_version: Option, pub session_start_time_ms: i64, } @@ -785,6 +785,69 @@ mod tests { .expect("migration metadata query should succeed") } + fn session_models_model_id_notnull(db: &TursoDb) -> i64 { + db.query_map("PRAGMA table_info(session_models)", (), |row| { + let name = row.get::(1)?; + let not_null = row.get::(3)?; + Ok((name, not_null)) + }) + .expect("session_models table info should load") + .into_iter() + .find_map(|(name, not_null)| (name == "model_id").then_some(not_null)) + .expect("session_models.model_id column should exist") + } + + #[test] + fn session_model_upsert_and_lookup_round_trip_nullable_and_present_model_ids() { + let db_path = unique_test_db_path(); + let db = AgentTraceDb::open_at(&db_path).expect("test DB should open"); + + assert_eq!(session_models_model_id_notnull(&db), 0); + + db.upsert_session_model(SessionModelUpsert { + tool_name: "claude", + session_id: "missing-model-session", + model_id: None, + tool_version: None, + session_start_time_ms: 1_800_000_000_000_i64, + }) + .expect("nullable model session upsert should succeed"); + db.upsert_session_model(SessionModelUpsert { + tool_name: "claude", + session_id: "model-present-session", + model_id: Some("claude/sonnet-4"), + tool_version: Some("Claude Code 1.2.3"), + session_start_time_ms: 1_800_000_001_000_i64, + }) + .expect("model-present session upsert should succeed"); + + let missing_model = db + .session_model_by_tool_and_session("claude", "missing-model-session") + .expect("nullable model session lookup should succeed") + .expect("nullable model session row should exist"); + assert_eq!(missing_model.model_id, None); + assert_eq!(missing_model.tool_version, None); + assert_eq!(missing_model.session_start_time_ms, 1_800_000_000_000_i64); + + let model_present = db + .session_model_by_tool_and_session("claude", "model-present-session") + .expect("model-present session lookup should succeed") + .expect("model-present session row should exist"); + assert_eq!( + model_present.model_id, + Some(String::from("claude/sonnet-4")) + ); + assert_eq!( + model_present.tool_version, + Some(String::from("Claude Code 1.2.3")) + ); + + drop(db); + if let Some(parent) = db_path.parent() { + fs::remove_dir_all(parent).expect("test DB directory should be removed"); + } + } + #[test] fn recent_diff_trace_patches_applies_bounded_window_ordering_and_parse_accounting() { let db_path = unique_test_db_path(); diff --git a/cli/src/services/hooks/mod.rs b/cli/src/services/hooks/mod.rs index 934fde79..06ec195e 100644 --- a/cli/src/services/hooks/mod.rs +++ b/cli/src/services/hooks/mod.rs @@ -63,7 +63,7 @@ struct SessionModelPayload { #[serde(rename = "sessionID")] session_id: String, time: u64, - model_id: String, + model_id: Option, tool_name: String, tool_version: Option, } @@ -788,7 +788,7 @@ where model_id: payload.model_id.clone().or_else(|| { session_attribution .as_ref() - .map(|attribution| attribution.model_id.clone()) + .and_then(|attribution| attribution.model_id.clone()) }), tool_version: payload.tool_version.clone().or_else(|| { session_attribution @@ -818,7 +818,6 @@ fn run_session_model_subcommand_from_payload( logger: Option<&dyn Logger>, ) -> Result { let payload = parse_session_model_payload(stdin_payload)?; - // Convert the u64 time to i64 for DB storage. let session_start_time_ms = i64::try_from(payload.time).map_err(|_| { anyhow!(StdinPayloadKind::SessionModel.validation_error( @@ -829,7 +828,7 @@ fn run_session_model_subcommand_from_payload( let upsert_payload = SessionModelUpsert { tool_name: &payload.tool_name, session_id: &payload.session_id, - model_id: &payload.model_id, + model_id: payload.model_id.as_deref(), tool_version: payload.tool_version.as_deref(), session_start_time_ms, }; @@ -1001,8 +1000,9 @@ where payload_kind.validation_error(d) })?; let time = required_u64_millisecond_field(payload, "time", payload_kind)?; - let model_id = - required_non_empty_string_field(payload, "model_id", |d| payload_kind.validation_error(d))?; + let model_id = Some(required_non_empty_string_field(payload, "model_id", |d| { + payload_kind.validation_error(d) + })?); let tool_name = required_non_empty_string_field(payload, "tool_name", |d| { payload_kind.validation_error(d) })?; @@ -1041,7 +1041,7 @@ fn parse_claude_session_model_payload( } let session_id = required_claude_session_id(payload, payload_kind)?; - let model_id = required_claude_model_id(payload, payload_kind)?; + let model_id = optional_claude_model_id(payload); let time = extract_claude_event_time(payload); let tool_name = "claude".to_string(); let tool_version = extract_claude_tool_version_from_payload(payload).or_else(|| { @@ -1076,17 +1076,14 @@ fn required_claude_session_id( )) } -fn required_claude_model_id( - payload: &serde_json::Map, - payload_kind: StdinPayloadKind, -) -> Result { +fn optional_claude_model_id(payload: &serde_json::Map) -> Option { // Try direct string fields first. for key in ["model", "model_id", "modelId"] { if let Some(value) = payload.get(key) { if let Some(s) = value.as_str() { let trimmed = s.trim(); if !trimmed.is_empty() { - return Ok(normalize_claude_model_id(trimmed)); + return Some(normalize_claude_model_id(trimmed)); } } // If model is an object, try nested identifier fields. @@ -1096,7 +1093,7 @@ fn required_claude_model_id( if let Some(s) = nested_value.as_str() { let trimmed = s.trim(); if !trimmed.is_empty() { - return Ok(normalize_claude_model_id(trimmed)); + return Some(normalize_claude_model_id(trimmed)); } } } @@ -1105,9 +1102,7 @@ fn required_claude_model_id( } } - bail!(payload_kind.validation_error( - "missing non-empty model identifier (model, model_id, or model.id) for Claude SessionStart" - )) + None } fn normalize_claude_model_id(model: &str) -> String { @@ -2821,13 +2816,13 @@ mod tests { } fn session_model_attribution( - model_id: &str, + model_id: Option<&str>, tool_version: Option<&str>, ) -> SessionModelAttribution { SessionModelAttribution { tool_name: String::from("claude"), session_id: String::from("session-123"), - model_id: model_id.to_string(), + model_id: model_id.map(String::from), tool_version: tool_version.map(String::from), session_start_time_ms: 1_800_000_000_000_i64, } @@ -2845,7 +2840,7 @@ mod tests { .expect("Claude SessionStart payload should parse"); assert_eq!(output.session_id, "session-123"); - assert_eq!(output.model_id, "claude/sonnet-4"); + assert_eq!(output.model_id, Some(String::from("claude/sonnet-4"))); assert_eq!(output.tool_name, "claude"); assert_eq!(output.tool_version, Some(String::from("1.2.3"))); } @@ -2897,7 +2892,7 @@ mod tests { assert_eq!(tool_name, "claude"); assert_eq!(session_id, "session-123"); Ok(Some(session_model_attribution( - "session-model", + Some("session-model"), Some("Claude Code 1.2.3"), ))) }) @@ -2916,7 +2911,7 @@ mod tests { let resolved = resolve_diff_trace_attribution(&payload, |_tool_name, _session_id| { Ok(Some(session_model_attribution( - "session-model", + Some("session-model"), Some("stored-version"), ))) }) diff --git a/context/architecture.md b/context/architecture.md index c0d32a0f..86f81295 100644 --- a/context/architecture.md +++ b/context/architecture.md @@ -116,7 +116,7 @@ The repository includes a new placeholder Rust binary crate at `cli/`. - `cli/src/services/db/mod.rs` provides the shared generic Turso infrastructure seam: `DbSpec` supplies a service-specific name, path, ordered embedded migrations, and config-file lookup key (`db_config_key()`), while `TursoDb` owns parent-directory creation, `Builder::new_local(...)` initialization (with `experimental_multiprocess_wal(true)` for safe concurrent access), Turso connection setup, tokio current-thread runtime bridging, retry-backed blocking `execute`/`query`/`query_map` wrappers, and generic migration execution with per-database `__sce_migrations` metadata. `TursoDb::new()` and `EncryptedTursoDb::new()` wrap only their local open/connect block in `run_with_retry_sync` using a config-driven connection-open policy resolved from the `DATABASE_RETRY_CONFIG` `OnceLock` with fallback to hardcoded defaults, while both adapters' `execute()`, `query()`, and `query_map()` methods use a config-driven operation policy from the same source. Both adapters' `query_map()` methods retry the initial query and row-fetch loop, then apply caller row mapping after retry completion. Migration execution is not retried. The same module also provides `EncryptedTursoDb`, a structurally parallel encrypted adapter that resolves the encryption key through `encryption_key::get_or_create_encryption_key()`, enables Turso local encryption with strict `aegis256` cipher selection, and exposes retry-backed synchronous wrappers plus migration execution. `cli/src/services/db/encryption_key.rs` first derives a Turso-compatible 64-character hex key from non-empty `SCE_AUTH_DB_ENCRYPTION_KEY` env-secret text when present, otherwise falls back to keyring-backed credential-store get-or-create behavior; no plaintext auth DB fallback exists. - `cli/src/services/local_db/mod.rs` provides the concrete local DB spec and `LocalDb` type alias over the shared generic `TursoDb` adapter. `LocalDbSpec` resolves the deterministic persistent runtime DB target through the shared default-path seam and declares no local migrations; `TursoDb` supplies retry-backed blocking `execute`/`query`, parent-directory creation, Turso connection setup, tokio current-thread runtime bridging, and generic migration execution. - `cli/src/services/auth_db/mod.rs` provides the encrypted auth DB spec and `AuthDb` type alias over `EncryptedTursoDb`. `AuthDbSpec` resolves `/sce/auth.db` through the shared default-path seam and embeds ordered auth migrations. Auth DB lifecycle setup/doctor integration is wired through `AuthDbLifecycle`; auth command/token-storage reads/writes are directed through `token_storage.rs`. -- `cli/src/services/agent_trace_db/mod.rs` provides the Agent Trace DB spec and `AgentTraceDb` type alias over `TursoDb`. `AgentTraceDbSpec` resolves the legacy `/sce/agent-trace.db` fallback through the shared default-path seam and embeds ordered fresh-start migrations `001..016` for `diff_traces`, `post_commit_patch_intersections`, `agent_traces`, `messages`, `parts`, `session_models`, `diff_traces.payload_type`, indexes, and triggers. `agent_traces.agent_trace_id` is `NOT NULL UNIQUE`. The module provides explicit-path `open_at(path)` and `open_for_hooks_without_migrations_at(path)` helpers for per-checkout DB files plus `DiffTraceInsert<'_>`/`insert_diff_trace()` including the `payload_type` discriminator, `PostCommitPatchIntersectionInsert<'_>`/`insert_post_commit_patch_intersection()`, `AgentTraceInsert<'_>`/`insert_agent_trace()`, `InsertMessageInsert`/`insert_message()` plus `insert_messages()` for parent message inserts with duplicate `(session_id, message_id)` writes ignored, `InsertPartInsert`/`insert_part()` plus `insert_parts()` for append-only part text writes, `SessionModelUpsert<'_>`/`upsert_session_model()` plus lookup helpers for durable `session_models` attribution, and `recent_diff_trace_patches(cutoff_time_ms, end_time_ms)` for inclusive chronological `diff_traces` reads that dispatch on `payload_type`, parse valid raw patch or structured payload rows, and return skipped malformed-row reports. `cli/src/services/agent_trace_db/lifecycle.rs` registers Agent Trace DB setup/doctor lifecycle behavior: setup creates/reuses and registers checkout identity, resolves `/sce/agent-trace-{checkout_id}.db`, initializes it with `AgentTraceDb::open_at(path)`, and records `database_path`; active hook runtime still resolves checkout identity and lazily creates or upgrades the per-checkout DB when setup has not run or schema metadata is incomplete. +- `cli/src/services/agent_trace_db/mod.rs` provides the Agent Trace DB spec and `AgentTraceDb` type alias over `TursoDb`. `AgentTraceDbSpec` resolves the legacy `/sce/agent-trace.db` fallback through the shared default-path seam and embeds ordered fresh-start migrations `001..016` for `diff_traces`, `post_commit_patch_intersections`, `agent_traces`, `messages`, `parts`, `session_models` with nullable `model_id`, `diff_traces.payload_type`, indexes, and triggers. `agent_traces.agent_trace_id` is `NOT NULL UNIQUE`. The module provides explicit-path `open_at(path)` and `open_for_hooks_without_migrations_at(path)` helpers for per-checkout DB files plus `DiffTraceInsert<'_>`/`insert_diff_trace()` including the `payload_type` discriminator, `PostCommitPatchIntersectionInsert<'_>`/`insert_post_commit_patch_intersection()`, `AgentTraceInsert<'_>`/`insert_agent_trace()`, `InsertMessageInsert`/`insert_message()` plus `insert_messages()` for parent message inserts with duplicate `(session_id, message_id)` writes ignored, `InsertPartInsert`/`insert_part()` plus `insert_parts()` for append-only part text writes, `SessionModelUpsert<'_>`/`upsert_session_model()` plus lookup helpers for durable `session_models` attribution, and `recent_diff_trace_patches(cutoff_time_ms, end_time_ms)` for inclusive chronological `diff_traces` reads that dispatch on `payload_type`, parse valid raw patch or structured payload rows, and return skipped malformed-row reports. `cli/src/services/agent_trace_db/lifecycle.rs` registers Agent Trace DB setup/doctor lifecycle behavior: setup creates/reuses and registers checkout identity, resolves `/sce/agent-trace-{checkout_id}.db`, initializes it with `AgentTraceDb::open_at(path)`, and records `database_path`; active hook runtime still resolves checkout identity and lazily creates or upgrades the per-checkout DB when setup has not run or schema metadata is incomplete. - `cli/src/test_support.rs` provides a shared test-only temp-directory helper (`TestTempDir`) used by service tests that need filesystem fixtures. - `cli/src/services/setup/mod.rs` defines the setup command contract (`SetupMode`, `SetupTarget`, `SetupRequest`, CLI flag parser/validator), an `inquire`-backed interactive target prompter (`InquireSetupTargetPrompter`), setup dispatch outcomes (proceed/cancelled), compile-time embedded asset access (`EmbeddedAsset`, target-scoped iterators, required-hook asset iterators/lookups) generated by `cli/build.rs` from the ephemeral crate-local `cli/assets/generated/config/{opencode,claude}/**` mirror plus `cli/assets/hooks/**`, and focused internal support seams for install-flow vs prompt-flow logic; `cli/src/services/setup/command.rs` owns the `SetupCommand` payload used by the static `RuntimeCommand` enum and executes against any context implementing repo-root scoping. Its install engine/orchestrator stages embedded files and uses a unified remove-and-replace policy (removing existing targets before swapping staged content, with deterministic recovery guidance on swap failure and no backup artifact creation), and formats deterministic completion messaging; required-hook install orchestration (`install_required_git_hooks`) follows the same remove-and-replace policy (removing existing hooks before swapping staged content, with deterministic recovery guidance on swap failure). The setup command derives a repo-root-scoped context before aggregating static lifecycle provider `setup` dispatch across providers (config → local_db → auth_db → agent_trace_db → hooks when requested), so setup providers consume only repo-root access from the scoped context. - `cli/src/services/setup/mod.rs` keeps those responsibilities inside one file for now, but the current ownership split is explicit: the inline `install` module owns repository-path normalization, staging/swap install behavior, required-hook installation, and filesystem safety guards, while the inline `prompt` module owns interactive target selection and prompt styling. @@ -125,7 +125,7 @@ The repository includes a new placeholder Rust binary crate at `cli/`. - `cli/src/services/version/mod.rs` defines the version command parser/rendering contract (`parse_version_request`, `render_version`) with deterministic text output and stable JSON runtime-identification fields; `cli/src/services/version/command.rs` owns the `VersionCommand` payload used by the static `RuntimeCommand` enum. - `cli/src/services/completion/mod.rs` defines completion parser/rendering contract (`parse_completion_request`, `render_completion`) with deterministic Bash/Zsh/Fish script output aligned to current parser-valid command/flag surfaces; `cli/src/services/completion/command.rs` owns the `CompletionCommand` payload used by the static `RuntimeCommand` enum. - `cli/src/services/hooks/mod.rs` defines the current local hook runtime parsing/dispatch (`HookSubcommand`, `run_hooks_subcommand`) plus a commit-msg co-author policy seam (`apply_commit_msg_coauthor_policy`) that injects one canonical SCE trailer only when the enabled-by-default attribution-hooks config/env control is not opted out, `SCE_DISABLED` is false, and the staged-diff AI-overlap preflight confirms AI/editor evidence (`StagedDiffAiOverlapResult::Overlap`); the preflight is wired into `run_commit_msg_subcommand_in_repo` and logs `sce.hooks.commit_msg.ai_overlap_error` on error paths; `cli/src/services/hooks/command.rs` owns the `HooksCommand` payload used by the static `RuntimeCommand` enum. In the current attribution-only baseline, `pre-commit` and `post-rewrite` are deterministic no-op surfaces; `post-commit` requires validated `--remote-url`, threads that URL through the Agent Trace flow, prints it to stderr, and remains an active intersection + Agent Trace persistence entrypoint (captures current commit patch, queries recent `diff_traces` from the bounded past-7-days window, combines valid patches via `patch::combine_patches`, intersects with post-commit patch via `patch::intersect_patches`, persists result to `post_commit_patch_intersections`, then persists built Agent Trace payloads with range-level `content_hash` values to `agent_traces` in AgentTraceDb without post-commit file artifacts); `diff-trace` performs STDIN JSON intake, validates required non-empty `sessionID`/`diff`/`tool_name`, optional `model_id` (absent/`null` → `None`, resolved from `session_models` by `tool_name` + `session_id` when absent) and required `tool_version` (present and either `null` or non-empty string) plus required `u64` `time` (Unix epoch milliseconds), rejects values that cannot fit AgentTraceDb signed `time_ms` storage, writes one collision-safe parsed-payload `context/tmp/-000000-diff-trace.json` artifact, and inserts the parsed payload fields into AgentTraceDb; `session-model` performs STDIN JSON intake, validates required non-empty `sessionID`/`model_id`/`tool_name`, required `u64` `time` (Unix epoch milliseconds), and required nullable/non-empty `tool_version`, then upserts the parsed payload into AgentTraceDb `se... (line truncated to 2000 chars) -- Claude `SessionStart` session-model parsing in `cli/src/services/hooks/mod.rs` uses explicit payload version fields (`tool_version`/`claude_version`/`version`) when present; if no non-empty payload version is available, it best-effort runs `claude --version`, trims stdout, and leaves `tool_version` nullable without failing intake when the command is unavailable, fails, or returns empty output. +- Claude `SessionStart` session-model parsing in `cli/src/services/hooks/mod.rs` requires session identity but treats model attribution as optional; when a non-empty model identifier is present it normalizes the value with the `claude/` prefix, and when absent it persists nullable `model_id` without inventing a placeholder. It uses explicit payload version fields (`tool_version`/`claude_version`/`version`) when present; if no non-empty payload version is available, it best-effort runs `claude --version`, trims stdout, and leaves `tool_version` nullable without failing intake when the command is unavailable, fails, or returns empty output. - Diff-trace attribution resolution in `cli/src/services/hooks/mod.rs` looks up `session_models` when `model_id` or `tool_version` is missing/nullable, fills only missing fields from the stored row when available, preserves direct payload precedence, and continues persistence with `None` for unresolved attribution. - `cli/src/services/resilience.rs` defines bounded retry/timeout/backoff execution policy (`RetryPolicy`, `run_with_retry`) for transient operation hardening with deterministic failure messaging and retry observability. - No user-invocable `sce sync` command is wired in the current runtime; local DB bootstrap and setup-time per-checkout Agent Trace DB initialization flow through lifecycle providers aggregated by setup, while checkout/global DB health/repair and checkout DB discovery flow through the doctor surface. diff --git a/context/context-map.md b/context/context-map.md index f30c9a52..5deda32a 100644 --- a/context/context-map.md +++ b/context/context-map.md @@ -45,7 +45,7 @@ Feature/domain context: - `context/sce/local-db.md` (implemented `cli/src/services/local_db/mod.rs` local database spec with `LocalDb = TursoDb`, canonical local DB path resolution, zero local migrations, and inherited retry-backed blocking `execute`/`query`/`query_map` methods using the shared Turso adapter) - `context/sce/shared-turso-db.md` (current shared `cli/src/services/db/mod.rs` Turso database infrastructure seam, including `DbSpec`, generic `TursoDb`, encrypted `EncryptedTursoDb`, build-time generated migration constants from `cli/build.rs`/`cli/src/generated_migrations.rs`, config-driven constructor/open-connect retry via `run_with_retry_sync`, no-migration `TursoDb::open_without_migrations()` / explicit-path `open_without_migrations_at(path)` for hot runtime paths, migration-running `new()` / explicit-path `new_at(path)` / `run_migrations()` with per-database `__sce_migrations` tracking, config-driven operation retry for `execute`/`query`/`query_map` with a `<= 2_000ms` default query failure budget, row-mapping excluded from retry, generic embedded migration execution, non-mutating `migration_metadata_problems()` and `ensure_schema_ready(setup_guidance)` readiness methods on `TursoDb`, and concrete wrappers for `LocalDb`, `AuthDb`, plus `AgentTraceDb`) - `context/sce/auth-db.md` (encrypted `AuthDb = EncryptedTursoDb` adapter, canonical `/sce/auth.db` path, build-time generated `AUTH_MIGRATIONS` from `cli/migrations/auth/`, auth credential schema and updated-at trigger baseline, lifecycle setup/doctor integration, encrypted token-storage persistence, and `SCE_AUTH_DB_ENCRYPTION_KEY`/OS credential-store key handling) -- `context/sce/agent-trace-db.md` (implemented `cli/src/services/agent_trace_db/mod.rs` Agent Trace database wrapper with legacy global `/sce/agent-trace.db` fallback plus active per-checkout `/sce/agent-trace-{checkout_id}.db` paths, explicit-path migration and no-migration open APIs, non-mutating `ensure_schema_ready_for_hooks()` delegation to `TursoDb::ensure_schema_ready()` with `Run 'sce setup'.` guidance, setup-time per-checkout DB initialization plus hook-runtime lazy initialization/upgrade fallback, ordered `diff_traces`, `post_commit_patch_intersections`, `agent_traces`, parent `messages`, append-only `parts`, indexes/triggers, typed parameterized insert helpers, bounded chronological recent `diff_traces` query/parse support with malformed-row skip accounting, registered setup/doctor lifecycle provider, active hook writers for `diff_traces`, post-commit intersection/agent-trace persistence, `messages`, and `parts`, and `cli/src/services/agent_trace.rs` pure patch-overlap helper `patches_have_overlap` consumed by the staged-diff AI-overlap evidence gate in `cli/src/services/hooks/mod.rs`) +- `context/sce/agent-trace-db.md` (implemented `cli/src/services/agent_trace_db/mod.rs` Agent Trace database wrapper with legacy global `/sce/agent-trace.db` fallback plus active per-checkout `/sce/agent-trace-{checkout_id}.db` paths, explicit-path migration and no-migration open APIs, non-mutating `ensure_schema_ready_for_hooks()` delegation to `TursoDb::ensure_schema_ready()` with `Run 'sce setup'.` guidance, setup-time per-checkout DB initialization plus hook-runtime lazy initialization/upgrade fallback, ordered `diff_traces`, `post_commit_patch_intersections`, `agent_traces`, parent `messages`, append-only `parts`, `session_models` with nullable `model_id`, indexes/triggers, typed parameterized insert helpers, bounded chronological recent `diff_traces` query/parse support with malformed-row skip accounting, registered setup/doctor lifecycle provider, active hook writers for `diff_traces`, post-commit intersection/agent-trace persistence, `messages`, and `parts`, and `cli/src/services/agent_trace.rs` pure patch-overlap helper `patches_have_overlap` consumed by the staged-diff AI-overlap evidence gate in `cli/src/services/hooks/mod.rs`) - `context/sce/agent-trace-core-schema-migrations.md` (historical reference for removed local DB schema bootstrap behavior; T03 now implements the actual local DB with migrations) - `context/sce/agent-trace-retry-queue-observability.md` (inactive local-hook retry path plus historical retry/metrics reference) - `context/sce/agent-trace-local-hooks-mvp-contract-gap-matrix.md` (T01 Local Hooks MVP production contract freeze and deterministic gap matrix for `agent-trace-local-hooks-production-mvp`) diff --git a/context/glossary.md b/context/glossary.md index d073a996..c3c29583 100644 --- a/context/glossary.md +++ b/context/glossary.md @@ -38,7 +38,7 @@ - `encrypted Turso adapter`: Generic adapter seam in `cli/src/services/db/mod.rs` exposed as `EncryptedTursoDb`, structurally parallel to `TursoDb` (connection, tokio runtime bridge, spec typing). Its constructor resolves the encryption key via `encryption_key::get_or_create_encryption_key(&db_path, db_name)`, which derives a Turso-compatible 64-character hex key from non-empty `SCE_AUTH_DB_ENCRYPTION_KEY` env-secret text before falling back to OS credential-store keyring get-or-create behavior; credential-store default registration is guarded by stable `OnceLock` plus an atomic in-progress flag so errors or panics leave initialization retryable without mutex poisoning. The adapter enables Turso local encryption with strict `aegis256` cipher selection through `turso::EncryptionOpts`, wraps encrypted local open/connect in the default DB connection-open retry policy, and runs embedded migrations after retry has produced a connection; the adapter also exposes retry-backed synchronous `execute`, `query`, `query_map`, and `run_migrations` helpers with `__sce_migrations` tracking parity. - `auth DB adapter`: Module in `cli/src/services/auth_db/mod.rs` that defines `AuthDbSpec` and exposes `AuthDb` as an `EncryptedTursoDb` alias. It resolves the canonical `/sce/auth.db` path with `auth_db_path()`, keeps encryption mandatory with `SCE_AUTH_DB_ENCRYPTION_KEY` env-secret precedence before OS keyring fallback and no plaintext mode, and embeds ordered auth migrations where baseline SQL creates `auth_credentials` without `user_id`, with `updated_at`, and a trigger that auto-refreshes `updated_at` on row updates. Auth runtime token-storage is now wired through `cli/src/services/token_storage.rs`, which persists tokens via the `auth_credentials` table in the encrypted auth DB instead of a JSON file. - `AuthDbLifecycle`: Lifecycle provider in `cli/src/services/auth_db/lifecycle.rs` that implements `ServiceLifecycle` for encrypted auth DB setup/doctor integration. `diagnose` collects auth DB path health problems, `fix` bootstraps missing auth DB parent directory, and `setup` calls `AuthDb::new()`. Registered as `LifecycleProviderId::AuthDb` in the shared lifecycle catalog. -- `agent trace DB adapter`: Module in `cli/src/services/agent_trace_db/mod.rs` that defines `AgentTraceDbSpec`, exposes `AgentTraceDb` as a `TursoDb` alias, keeps `/sce/agent-trace.db` as the legacy/global fallback path, supports explicit per-checkout DB paths through `open_at(path)` and `open_for_hooks_without_migrations_at(path)`, embeds an ordered split fresh-start baseline migration set (`001..016`) covering `diff_traces`, `post_commit_patch_intersections`, `agent_traces`, `messages`, `parts`, `session_models`, and `diff_traces.payload_type` plus indexes and triggers; provides typed parameterized insert/upsert helpers for diff traces with `payload_type` discriminator, post-commit intersection rows, built agent-trace rows, message/part batch inserts, and session model attribution; exposes chronological recent `diff_traces` query/parse support with malformed-row skip accounting; has `AgentTraceDbLifecycle` for setup/doctor integration including setup-time per-checkout DB initialization; and is accessed by active Agent Trace hooks through checkout-scoped lazy DB resolution when setup has not prepared the DB. +- `agent trace DB adapter`: Module in `cli/src/services/agent_trace_db/mod.rs` that defines `AgentTraceDbSpec`, exposes `AgentTraceDb` as a `TursoDb` alias, keeps `/sce/agent-trace.db` as the legacy/global fallback path, supports explicit per-checkout DB paths through `open_at(path)` and `open_for_hooks_without_migrations_at(path)`, embeds an ordered split fresh-start baseline migration set (`001..016`) covering `diff_traces`, `post_commit_patch_intersections`, `agent_traces`, `messages`, `parts`, `session_models` with nullable `model_id`, and `diff_traces.payload_type` plus indexes and triggers; provides typed parameterized insert/upsert helpers for diff traces with `payload_type` discriminator, post-commit intersection rows, built agent-trace rows, message/part batch inserts, and session model attribution; exposes chronological recent `diff_traces` query/parse support with malformed-row skip accounting; has `AgentTraceDbLifecycle` for setup/doctor integration including setup-time per-checkout DB initialization; and is accessed by active Agent Trace hooks through checkout-scoped lazy DB resolution when setup has not prepared the DB. - `structured patch service`: Pure synchronous Rust service in `cli/src/services/structured_patch.rs` that derives supported structured editor hook payloads into canonical `ParsedPatch` values. The current implemented source is Claude `PostToolUse` payloads for `Write` creates and `Edit` structured patches; wired into `sce hooks diff-trace` for Claude payload classification at intake (T04) and into `AgentTraceDb::recent_diff_trace_patches` for post-commit structured payload parsing dispatch at read time (T05). - `Agent Trace SCE metadata`: Implementation-owned top-level metadata emitted by `build_agent_trace(...)` as `metadata.sce.version`; the value is sourced from the compiled `sce` CLI package version via `env!("CARGO_PKG_VERSION")`, is schema-validated with the rest of the payload, and is persisted in AgentTraceDb `agent_traces.trace_json` without changing the top-level Agent Trace payload/schema `version`. - `Agent Trace range content_hash`: Per-range `content_hash` emitted by `build_agent_trace(...)` inside every `ranges[]` entry as `murmur3:`, computed from the touched-line kind/content of the `post_commit_patch` or embedded-patch hunk used to emit that range while excluding positions, paths, metadata, and database IDs. @@ -140,7 +140,7 @@ - `agent trace historical reference docs`: Retained `context/sce/agent-trace-*.md` artifacts that describe the removed pre-v0.3 Agent Trace design and task slices; they are reference-only and do not describe the active local-hook runtime. - `agent trace commit-msg co-author policy`: Current contract in `cli/src/services/hooks/mod.rs` (`apply_commit_msg_coauthor_policy`) that applies exactly one canonical trailer (`Co-authored-by: SCE `) only when attribution hooks are enabled, SCE is not disabled, and the staged-diff AI-overlap preflight confirms AI/editor evidence (`StagedDiffAiOverlapResult::Overlap`); `NoOverlap` and `Error` both suppress the trailer, with `Error` logged via `sce.hooks.commit_msg.ai_overlap_error`; duplicate canonical trailers are deduped idempotently. - `local DB migration contract`: `cli/src/services/local_db/mod.rs` delegates migration execution to `TursoDb` through the `DbSpec::migrations()` contract. The current `LocalDbSpec` migration list is empty, so `LocalDb::new()` opens/creates the canonical local DB without creating local tables. -- `hook no-op baseline`: Current `cli/src/services/hooks/mod.rs` runtime posture where `pre-commit` and `post-rewrite` return deterministic no-op status text, `commit-msg` is a gated mutating path behind the enabled-by-default attribution-hooks control with explicit opt-out, `post-commit` requires validated `--remote-url`, threads that value through the Agent Trace flow, prints it to stderr, captures current commit patch, queries recent `diff_traces` from past 7 days, combines/intersects patches, persists to `post_commit_patch_intersections`, and persists built Agent Trace payloads to `agent_traces` without post-commit file artifacts, `diff-trace` is an active intake path (validates required STDIN payload fields including `sessionID`/`diff`/`tool_name`, optional `model_id`, required nullable/non-empty `tool_version`, fills missing/nullable attribution from `session_models` when available while preserving direct payload precedence, writes collision-safe parsed-payload `context/tmp/-000000-diff-trace.json` artifacts, and inserts parsed payload fields into AgentTraceDb with nullable/resolved attribution), and `session-model` is an active intake path (validates required STDIN payload fields including `sessionID`/`model_id`/`tool_name`, best-effort fills missing Claude `tool_version` from `claude --version`, and upserts into `session_models` without raw artifacts). +- `hook no-op baseline`: Current `cli/src/services/hooks/mod.rs` runtime posture where `pre-commit` and `post-rewrite` return deterministic no-op status text, `commit-msg` is a gated mutating path behind the enabled-by-default attribution-hooks control with explicit opt-out, `post-commit` requires validated `--remote-url`, threads that value through the Agent Trace flow, prints it to stderr, captures current commit patch, queries recent `diff_traces` from past 7 days, combines/intersects patches, persists to `post_commit_patch_intersections`, and persists built Agent Trace payloads to `agent_traces` without post-commit file artifacts, `diff-trace` is an active intake path (validates required STDIN payload fields including `sessionID`/`diff`/`tool_name`, optional `model_id`, required nullable/non-empty `tool_version`, fills missing/nullable attribution from `session_models` when available while preserving direct payload precedence, writes collision-safe parsed-payload `context/tmp/-000000-diff-trace.json` artifacts, and inserts parsed payload fields into AgentTraceDb with nullable/resolved attribution), and `session-model` is an active intake path (normalized payloads require `sessionID`/`model_id`/`tool_name`, Claude `SessionStart` payloads require session identity but may persist nullable `model_id`, missing Claude `tool_version` is best-effort filled from `claude --version`, and upserts into `session_models` without raw artifacts). - `sce doctor` operator-health contract: `cli/src/services/doctor/mod.rs` is the stable doctor entrypoint, with focused `doctor/{inspect,render,fixes,types}.rs` submodules implementing the current approved operator-health surface in `context/sce/agent-trace-hook-doctor.md`: `sce doctor --fix` selects repair intent, `sce doctor dbs` lists registered checkouts, help/output expose deterministic doctor mode/action, JSON includes stable problem taxonomy/fixability fields plus checkout/database records and fix-result records, the runtime validates state-root resolution, global and repo-local `sce/config.json` readability/schema health, local DB and checkout/global Agent Trace DB path/health, DB-parent readiness barriers, git availability, non-repo vs bare-repo targeting failures, effective hook-path source resolution, required hook presence/executable/content drift against canonical embedded hook assets, and repo-root installed OpenCode integration presence for `OpenCode plugins`, `OpenCode agents`, `OpenCode commands`, and `OpenCode skills`. Human text mode uses the approved sectioned layout (`Environment`, `Configuration` with checkout identity + Agent Trace checkout DB rows when available, `Repository`, `Git Hooks`, `Integrations`), `SCE doctor diagnose` / `SCE doctor fix` headers, bracketed `[PASS]`/`[FAIL]`/`[MISS]` status tokens with shared-style green/red colorization when enabled, simplified `label (path)` row formatting, top-level-only hook rows, and presence-only integration parent/child rows where missing required files surface as `[MISS]` children and `[FAIL]` parent groups. Fix mode reuses canonical setup hook installation for missing/stale/non-executable required hooks and missing hooks directories and can bootstrap canonical missing SCE-owned DB parent directories. - `cli warnings-denied lint policy`: `cli/Cargo.toml` sets `warnings = "deny"`, so plain `cargo clippy --manifest-path cli/Cargo.toml` already fails on warnings without needing an extra `-- -D warnings` tail. - `agent trace local DB schema migration contract`: Retired `apply_core_schema_migrations` behavior removed from the current runtime during `agent-trace-removal-and-hook-noop-reset` T01; the local DB baseline is now file open/create only. diff --git a/context/plans/claude-session-model-missing-model.md b/context/plans/claude-session-model-missing-model.md new file mode 100644 index 00000000..5413c2af --- /dev/null +++ b/context/plans/claude-session-model-missing-model.md @@ -0,0 +1,95 @@ +# Claude Session Model Missing Model + +## Change summary + +Claude Code can emit a raw `SessionStart` hook event without a `model` property when a new session starts via `/clear`. The current `sce hooks session-model` Claude intake treats the model identifier as required and fails before hook completion. Update session-model attribution so `model_id` and `tool_version` are both optional in the `session_models` upsert path, allowing Claude `SessionStart` events to be recorded even when model attribution is unavailable. + +## Success criteria + +- Raw Claude `SessionStart` payloads with a valid `session_id` / `sessionID` and no usable `model`, `model_id`, `modelId`, or nested model identifier do not fail `sce hooks session-model`. +- Missing-model Claude `SessionStart` payloads still attempt a `session_models` upsert keyed by `(tool_name="claude", session_id)` with `model_id = NULL` and nullable `tool_version`. +- `session_models.model_id` is nullable in the database schema and Rust adapter types; existing model-present rows remain supported. +- Later diff-trace attribution fallback treats nullable stored `model_id` the same way it treats nullable `tool_version`: fill only fields that are present in the stored row, and continue with `None` when unresolved. +- Existing Claude `SessionStart` payloads with a model identifier still normalize and persist `model_id` as before, including the `claude/` prefix behavior. +- Existing normalized OpenCode/session-model payload validation remains unchanged unless implementation proves the shared adapter type must accept optional values while the OpenCode parser still enforces required `model_id` before building its payload. +- Unit coverage captures the Claude `/clear`-style missing-model upsert case, existing model-present behavior, nullable lookup behavior, and unchanged OpenCode validation. +- Current-state context for `session-model` hook behavior is updated if the implemented behavior changes documented contracts. + +## Constraints and non-goals + +- Keep runtime behavior changes focused on session-model attribution storage and its existing diff-trace fallback consumer; do not change `conversation-trace`, `commit-msg`, or `post-commit` behavior. +- A database migration to allow nullable `session_models.model_id` is in scope. +- Do not invent placeholder model IDs such as `unknown`; missing Claude model attribution should not create false model provenance. +- Do not change generated Claude hook registration unless code investigation shows the generated hook command itself is wrong. +- Preserve existing error behavior for malformed JSON, unsupported Claude hook events, and missing Claude session identity. + +## Task stack + +- [x] T01: `Make session model IDs nullable in storage` (status:done) + - Task ID: T01 + - Goal: Update the Agent Trace DB `session_models` schema and adapter types so stored `model_id` can be `NULL` while `tool_version` remains nullable. + - Boundaries (in/out of scope): In - a forward Agent Trace DB migration, `SessionModelUpsert` / `SessionModelAttribution` Rust type updates, SQL parameter/row mapping updates, and adapter-level tests. Out - Claude/OpenCode hook parser behavior changes, generated config rewrites, and unrelated DB tables. + - Done when: New and migrated Agent Trace DBs allow `session_models.model_id = NULL`; adapter upsert and lookup helpers accept/return `Option` for `model_id`; existing non-null model rows still round-trip; migration metadata stays deterministic. + - Verification notes (commands or checks): Run focused Agent Trace DB tests through Nix, for example `nix develop -c sh -c 'cd cli && cargo test agent_trace_db'`, plus inspect generated migration ordering expectations if tests cover migration manifests. + - Completed: 2026-06-29 + - Files changed: `cli/migrations/agent-trace/015_create_session_models.sql`, `cli/src/services/agent_trace_db/mod.rs`, `cli/src/services/hooks/mod.rs` + - Evidence: `nix develop -c sh -c 'cd cli && cargo fmt'`; focused `cargo test agent_trace_db` command was blocked by repo bash policy; `nix flake check` passed; `nix run .#pkl-check-generated` passed. + - Notes: Because the Agent Trace DB has not been released, the existing `015_create_session_models.sql` migration was updated in place instead of adding a new migration. Adapter storage types now accept/return nullable `model_id`; hook call sites preserve existing parser behavior by passing `Some(...)` for current model-present payloads. + +- [x] T02: `Persist Claude SessionStart without model` (status:done) + - Task ID: T02 + - Goal: Make `sce hooks session-model` persist Claude `SessionStart` events even when model attribution is absent, using nullable `model_id` and nullable `tool_version`. + - Boundaries (in/out of scope): In - `cli/src/services/hooks/mod.rs` session-model parsing/runtime flow, diff-trace fallback handling for optional stored `model_id`, targeted unit tests, and current-state context updates for the changed hook contract. Out - generated Claude hook registration, OpenCode normalized input relaxation, and unrelated hook runtime changes. + - Done when: A Claude `SessionStart` payload with `session_id` but no model identifier returns the normal successful session-model intake result and attempts a `SessionModelUpsert` with `model_id = None`; model-present Claude payloads still persist normalized `Some("claude/...")`; normalized OpenCode payloads still require `model_id`; later diff-trace fallback fills model only when the stored session row has one. + - Verification notes (commands or checks): Run a focused hooks/session-model test group through Nix, for example `nix develop -c sh -c 'cd cli && cargo test claude_session_model'`, and review `context/sce/agent-trace-hooks-command-routing.md`, `context/sce/agent-trace-db.md`, `context/architecture.md`, and related current-state context for required wording updates. + - Completed: 2026-06-29 + - Files changed: `cli/src/services/hooks/mod.rs`, `context/architecture.md`, `context/glossary.md`, `context/sce/agent-trace-db.md`, `context/sce/agent-trace-hooks-command-routing.md` + - Evidence: `nix develop -c sh -c 'cd cli && cargo fmt'`; focused `cargo test claude_session_model` command was blocked by repo bash policy; `nix run .#pkl-check-generated` passed; `nix flake check` passed. + - Notes: Claude raw `SessionStart` parsing now treats model attribution as optional and passes nullable `model_id` into `SessionModelUpsert`; normalized OpenCode parsing still requires `model_id`; diff-trace fallback uses nullable stored `model_id` only when available. Newly added unit tests were removed at user request, so T02 relies on existing test coverage plus full flake validation rather than retaining new targeted test cases. + +- [x] T03: `Validate and clean up` (status:done) + - Task ID: T03 + - Goal: Run final repository validation and remove any temporary scaffolding left from T01. + - Boundaries (in/out of scope): In - full repo validation, generated-output parity check, review of plan status/evidence, and cleanup of temporary local artifacts. Out - additional behavior changes beyond fixes needed to make the planned checks pass. + - Done when: Required validation commands complete successfully or failures are documented with actionable follow-up; no temporary test/debug artifacts remain; plan task statuses and verification evidence are ready for handoff/closure; context sync has been checked for current-state accuracy. + - Verification notes (commands or checks): `nix run .#pkl-check-generated`; `nix flake check`; inspect `git diff` for unintended files and confirm current-state context remains aligned with code truth. + - Completed: 2026-06-29 + - Files changed: `context/plans/claude-session-model-missing-model.md` + - Evidence: `nix run .#pkl-check-generated` passed; `nix flake check` passed; `git status --short` and `git diff --stat` inspected. + - Notes: Existing ignored `context/tmp/` runtime artifacts were observed but no tracked temporary/debug scaffolding requiring cleanup was found. Context sync classified this as verify-only unless final sync detects drift. + +## Open questions + +- None. + +## Validation Report + +### Commands run + +- `git status --short` -> exit 0; reviewed modified implementation/context files and untracked plan file. +- `git diff --stat` -> exit 0; reviewed implementation/context diff summary for unintended tracked files. +- `nix run .#pkl-check-generated` -> exit 0; output included `Generated outputs are up to date.` +- `nix flake check` -> exit 0; output included `all checks passed!` + +### Success-criteria verification + +- [x] Raw Claude `SessionStart` payloads without usable model attribution do not fail session-model parsing: verified in `cli/src/services/hooks/mod.rs` where `optional_claude_model_id(...)` returns `None` instead of raising validation failure. +- [x] Missing-model Claude `SessionStart` payloads attempt `session_models` upsert with `model_id = NULL`: verified by `SessionModelUpsert { model_id: payload.model_id.as_deref(), ... }` and nullable adapter storage. +- [x] `session_models.model_id` is nullable in schema and Rust adapter types: verified in `cli/migrations/agent-trace/015_create_session_models.sql`, `SessionModelUpsert`, and `SessionModelAttribution`. +- [x] Diff-trace attribution fallback only fills stored fields when present: verified by `resolve_diff_trace_attribution(...)` using nullable `SessionModelAttribution.model_id` and preserving unresolved `None`. +- [x] Model-present Claude payloads still normalize with `claude/` prefix: covered by existing `claude_session_model_payload_prefers_payload_tool_version_without_cli_probe` assertion. +- [x] Normalized OpenCode/session-model validation remains unchanged: verified in `parse_normalized_session_model_payload(...)`, which still requires non-empty `model_id` before constructing `Some(model_id)`. +- [x] Context updated for current behavior: verified in `context/architecture.md`, `context/glossary.md`, `context/context-map.md`, `context/sce/agent-trace-db.md`, and `context/sce/agent-trace-hooks-command-routing.md`. + +### Temporary scaffolding cleanup + +- No tracked temporary test/debug scaffolding was found during worktree/diff review. +- Existing ignored `context/tmp/` runtime artifacts were observed and left in place because they were not tracked plan scaffolding. + +### Failed checks and follow-ups + +- None. Required validation commands passed. + +### Residual risks + +- The plan's desired missing-model parser unit coverage is represented indirectly through code review plus full flake validation; additional focused unit coverage was removed during T02 at user request. diff --git a/context/sce/agent-trace-db.md b/context/sce/agent-trace-db.md index 678a1889..5c2320eb 100644 --- a/context/sce/agent-trace-db.md +++ b/context/sce/agent-trace-db.md @@ -33,9 +33,9 @@ pub type AgentTraceDb = TursoDb; - `INSERT_PART_SQL`: parameterized single-row append-only INSERT into `parts` (no upsert; multiple rows per `(session_id, message_id)` allowed). - `insert_part(input)`: typed single-row helper that inserts a part row without requiring a matching `messages` row (supports out-of-order writes); retained as part of the adapter surface. - `insert_parts(inputs)`: typed batch helper that generates and executes one parameterized multi-row append-only `parts` insert for valid conversation-trace `message.part` batches. -- `SessionModelUpsert<'a>`: upsert payload with `tool_name`, `session_id`, `model_id`, nullable `tool_version`, and `session_start_time_ms`. +- `SessionModelUpsert<'a>`: upsert payload with `tool_name`, `session_id`, nullable `model_id`, nullable `tool_version`, and `session_start_time_ms`. - `upsert_session_model()`: domain-specific upsert helper for `session_models` keyed by `(tool_name, session_id)`. -- `SessionModelAttribution`: durable session model attribution row returned from `session_models` lookups, carrying `model_id` plus nullable `tool_version` for later diff-trace fallback. +- `SessionModelAttribution`: durable session model attribution row returned from `session_models` lookups, carrying nullable `model_id` plus nullable `tool_version` for later diff-trace fallback. - `session_model_by_tool_and_session()`: lookup helper for model/tool-version attribution by `(tool_name, session_id)`. - `lifecycle.rs`: service lifecycle provider for setup/doctor integration. @@ -151,7 +151,7 @@ The `session_models` migration creates durable editor session model attribution: - `id INTEGER PRIMARY KEY` - `tool_name TEXT NOT NULL` - `session_id TEXT NOT NULL` -- `model_id TEXT NOT NULL` +- `model_id TEXT` (nullable) - `tool_version TEXT` (nullable) - `session_start_time_ms INTEGER NOT NULL` - `created_at TEXT NOT NULL DEFAULT (...)` @@ -208,7 +208,7 @@ Post-commit intersection rows are written by the active `post-commit` hook flow - No `context/tmp` artifact is written for conversation traces. - The generated OpenCode agent-trace plugin sends mixed-batch envelopes for conversation traces: regular `message` and `message.part` events each carry one per-item `type`, while diff-backed `message` events send one envelope containing the synthetic parent message item plus patch part items. -The `sce hooks session-model` command route writes session-model attribution payloads into `session_models` via STDIN JSON intake with required `sessionID`/`time`/`model_id`/`tool_name` and nullable `tool_version`; Claude `SessionStart` raw payloads may fill missing version metadata from a best-effort `claude --version` probe before upsert. The stored nullable `session_models.tool_version` is the durable fallback reused by later diff-trace persistence when the incoming payload omits version metadata. `(tool_name, session_id)` is the unique upsert key: subsequent upserts for the same tool/session pair replace `model_id`, `tool_version`, and `session_start_time_ms` while updating `updated_at`. See [agent-trace-hooks-command-routing.md](agent-trace-hooks-command-routing.md). +The `sce hooks session-model` command route writes session-model attribution payloads into `session_models` via STDIN JSON intake. Current parsing still requires normalized OpenCode payloads to provide `sessionID`/`time`/`model_id`/`tool_name` while allowing nullable `tool_version`; Claude `SessionStart` raw payloads require session identity but accept absent/empty model attribution and may fill missing version metadata from a best-effort `claude --version` probe before upsert. Storage accepts nullable `model_id` and nullable `tool_version` so missing attribution can be represented as `NULL` instead of a placeholder. The stored nullable `session_models.model_id` and `session_models.tool_version` values are durable fallbacks reused by later diff-trace persistence only when available and when incoming payloads omit that metadata. `(tool_name, session_id)` is the unique upsert key: subsequent upserts for the same tool/session pair replace `model_id`, `tool_version`, and `session_start_time_ms` while updating `updated_at`. See [agent-trace-hooks-command-routing.md](agent-trace-hooks-command-routing.md). ## Recent patch reads diff --git a/context/sce/agent-trace-hooks-command-routing.md b/context/sce/agent-trace-hooks-command-routing.md index 89d1411f..9e657d16 100644 --- a/context/sce/agent-trace-hooks-command-routing.md +++ b/context/sce/agent-trace-hooks-command-routing.md @@ -110,9 +110,9 @@ - Current success output reports deterministic mixed-batch accounting: `conversation-trace hook persisted mixed payload batch to AgentTraceDb: attempted=, persisted_messages=, persisted_parts=, skipped=.` The hook does not persist `context/tmp` artifacts. - The generated OpenCode agent-trace plugin emits this mixed-batch shape for conversation-trace handoff: ordinary message/part events produce one-item mixed envelopes, completed question-tool parts produce `message.part` items with `part_type: "question"`, and diff-backed message events produce one envelope containing the synthetic parent `message` item plus patch `message.part` items. - `session-model` reads STDIN JSON and classifies the payload: - - **Claude `SessionStart` payloads** (detected by presence of top-level `hook_event_name`): extracts `session_id` from `session_id`/`sessionID`, `model_id` from `model`/`model_id` (including nested `model.id`/`model.model`/`model.name` with `claude/` prefix normalization), `time` from `time`/`timestamp` (falls back to current system time), `tool_name="claude"`, and `tool_version` from `tool_version`/`claude_version`/`version`; when no non-empty payload version is present, Rust best-effort runs `claude --version`, trims stdout, and uses that value if non-empty, otherwise leaving `tool_version` nullable without failing intake. + - **Claude `SessionStart` payloads** (detected by presence of top-level `hook_event_name`): extracts required `session_id` from `session_id`/`sessionID`, optional `model_id` from `model`/`model_id` (including nested `model.id`/`model.model`/`model.name` with `claude/` prefix normalization when present), `time` from `time`/`timestamp` (falls back to current system time), `tool_name="claude"`, and `tool_version` from `tool_version`/`claude_version`/`version`; missing or empty Claude model attribution is persisted as nullable `model_id` instead of failing intake, and when no non-empty payload version is present, Rust best-effort runs `claude --version`, trims stdout, and uses that value if non-empty, otherwise leaving `tool_version` nullable without failing intake. - **OpenCode normalized payloads** (no `hook_event_name`): existing `{ sessionID, time, model_id, tool_name, tool_version }` validation applies unchanged. - - Valid payloads are upserted into the per-checkout AgentTraceDb `session_models` via `SessionModelUpsert` using `(tool_name, session_id)` as the unique key. No raw hook artifacts are written. DB open/insert failures are logged through `sce.hooks.session_model.agent_trace_db_write_failed` and reported in the success text as failed persistence. + - Valid payloads are upserted into the per-checkout AgentTraceDb `session_models` via `SessionModelUpsert` using `(tool_name, session_id)` as the unique key. Storage accepts nullable `model_id` and nullable `tool_version`; normalized OpenCode payloads still require `model_id`, while Claude `SessionStart` may upsert `model_id = NULL`. No raw hook artifacts are written. DB open/insert failures are logged through `sce.hooks.session_model.agent_trace_db_write_failed` and reported in the success text as failed persistence. ## Explicit non-goals in the current baseline