From dc264d4f17987846a7d49dbe40207324bccc9be1 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Thu, 2 Jul 2026 09:12:17 +0000 Subject: [PATCH 1/4] feat(hooks): record route metadata --- src/daemon.rs | 22 ++++++++++ src/hooks/cursor.rs | 14 ++++--- src/hooks/kiro.rs | 7 ++-- src/hooks/mod.rs | 34 ++++++++++++++++ src/hooks/post_tool_use.rs | 8 ++-- src/mcp/hook_events.rs | 40 +++++++++++++++++++ src/mcp/server.rs | 29 +++++++++++++- src/mcp/tool_analytics.rs | 82 ++++++++++++++++++++++++++++++++++++++ 8 files changed, 224 insertions(+), 12 deletions(-) diff --git a/src/daemon.rs b/src/daemon.rs index d4878546..bf9d1463 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -86,6 +86,20 @@ impl HookAgent { } } +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct HookRouteMetadata { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub session_id: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub thread_id: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub cwd: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub worktree: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub branch: Option, +} + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct DaemonHookEvent { pub agent: String, @@ -96,6 +110,8 @@ pub struct DaemonHookEvent { pub command: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub cwd: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub route: Option, } impl DaemonHookEvent { @@ -112,9 +128,15 @@ impl DaemonHookEvent { rel_paths, command, cwd, + route: None, } } + pub fn with_route(mut self, route: Option) -> Self { + self.route = route; + self + } + pub fn cursor_after_file_edit(rel_paths: Vec) -> Self { Self::new(HookAgent::Cursor, "afterFileEdit", rel_paths, None, None) } diff --git a/src/hooks/cursor.rs b/src/hooks/cursor.rs index 12d72d2a..06873c56 100644 --- a/src/hooks/cursor.rs +++ b/src/hooks/cursor.rs @@ -20,8 +20,9 @@ use super::steering::{ }; use super::tool_hints::{decide_hint, HintAgent, ToolHint, ToolHintInput}; use super::{ - deduped_project_hint, event_session_id, format_tool_hint, nearest_project_like_root, - read_hook_event, record_hint_analytics, record_hook_invoked, rel_under_root, text_field, + deduped_project_hint, event_session_id, format_tool_hint, hook_route_metadata_from_event, + nearest_project_like_root, read_hook_event, record_hint_analytics, record_hook_invoked, + rel_under_root, text_field, }; /// Largest tail the `beforeSubmitPrompt` hot path will read in one call. Larger @@ -510,7 +511,8 @@ async fn notify_cursor_after_file_edit(event_json: &str) { } crate::daemon::notify_hook_event( &root, - crate::daemon::DaemonHookEvent::cursor_after_file_edit(rels), + crate::daemon::DaemonHookEvent::cursor_after_file_edit(rels) + .with_route(hook_route_metadata_from_event(event_json, &root)), ) .await; } @@ -533,7 +535,8 @@ async fn notify_cursor_after_shell_event(event_json: &str) { let cwd = cursor_event_cwd(&parsed).unwrap_or_else(|| root.clone()); crate::daemon::notify_hook_event( &root, - crate::daemon::DaemonHookEvent::cursor_after_shell_execution(command.to_string(), cwd), + crate::daemon::DaemonHookEvent::cursor_after_shell_execution(command.to_string(), cwd) + .with_route(hook_route_metadata_from_event(event_json, &root)), ) .await; } @@ -548,7 +551,8 @@ async fn notify_cursor_workspace_open(event_json: &str) { } crate::daemon::notify_hook_event( &root, - crate::daemon::DaemonHookEvent::cursor_workspace_open(root.clone()), + crate::daemon::DaemonHookEvent::cursor_workspace_open(root.clone()) + .with_route(hook_route_metadata_from_event(event_json, &root)), ) .await; } diff --git a/src/hooks/kiro.rs b/src/hooks/kiro.rs index 44f51594..564d92d8 100644 --- a/src/hooks/kiro.rs +++ b/src/hooks/kiro.rs @@ -11,8 +11,8 @@ use serde_json::Value; use super::claude::is_code_research_prompt; use super::tool_hints::{decide_hint, HintAgent, ToolHintInput}; use super::{ - event_cwd, event_cwd_from_parsed, event_session_id, read_hook_event, record_hook_invoked, - rel_under_root, research_block_reason, + event_cwd, event_cwd_from_parsed, event_session_id, hook_route_metadata_from_event, + read_hook_event, record_hook_invoked, rel_under_root, research_block_reason, }; /// Largest transcript tail the Kiro `userPromptSubmit` hook will read per call. @@ -188,7 +188,8 @@ async fn notify_kiro_post_tool_use(event_json: &str) { let rel_paths = kiro_post_tool_use_rel_paths(event_json, &project_root); crate::daemon::notify_hook_event( &project_root, - crate::daemon::DaemonHookEvent::kiro_post_tool_use(rel_paths, event_cwd(event_json)), + crate::daemon::DaemonHookEvent::kiro_post_tool_use(rel_paths, event_cwd(event_json)) + .with_route(hook_route_metadata_from_event(event_json, &project_root)), ) .await; } diff --git a/src/hooks/mod.rs b/src/hooks/mod.rs index 8dfdbb7e..3108caa0 100644 --- a/src/hooks/mod.rs +++ b/src/hooks/mod.rs @@ -236,6 +236,40 @@ fn record_hint_emitted( record_hint_analytics(root, "hint_emitted", agent, session_id, hint); } +fn hook_route_metadata_from_event( + event_json: &str, + project_root: &Path, +) -> Option { + let parsed = serde_json::from_str::(event_json).ok()?; + hook_route_metadata_from_parsed(&parsed, project_root) +} + +fn hook_route_metadata_from_parsed( + parsed: &Value, + project_root: &Path, +) -> Option { + let cwd = event_cwd_from_parsed(parsed); + let route_root = cwd.as_deref().unwrap_or(project_root); + let worktree = crate::worktree::git_worktree_root(route_root) + .unwrap_or_else(|| project_root.to_path_buf()); + let branch = crate::branch::current_branch(&worktree); + Some(crate::daemon::HookRouteMetadata { + session_id: event_session_id(parsed), + thread_id: text_field( + parsed, + &[ + "thread_id", + "threadId", + "conversation_thread_id", + "conversationThreadId", + ], + ), + cwd, + worktree: Some(worktree), + branch, + }) +} + fn deduped_project_hint( root: Option, agent: HintAgent, diff --git a/src/hooks/post_tool_use.rs b/src/hooks/post_tool_use.rs index d56b79b9..8e66aea2 100644 --- a/src/hooks/post_tool_use.rs +++ b/src/hooks/post_tool_use.rs @@ -11,7 +11,7 @@ use std::path::Path; use serde_json::Value; -use super::{codex, event_cwd_from_parsed, rel_under_root}; +use super::{codex, event_cwd_from_parsed, hook_route_metadata_from_parsed, rel_under_root}; /// Claude Code tools whose `PostToolUse` events the hook consumes. The /// installer's `PostToolUse` matcher is derived from this list so the matcher @@ -86,7 +86,8 @@ pub(crate) async fn notify_post_tool_use(spec: &PostToolUseSpec, event_json: &st } crate::daemon::notify_hook_event( &root, - crate::daemon::DaemonHookEvent::post_tool_use_edit(spec.agent, rels, cwd), + crate::daemon::DaemonHookEvent::post_tool_use_edit(spec.agent, rels, cwd) + .with_route(hook_route_metadata_from_parsed(&parsed, &root)), ) .await; } else if (spec.is_shell_tool)(tool_name) { @@ -100,7 +101,8 @@ pub(crate) async fn notify_post_tool_use(spec: &PostToolUseSpec, event_json: &st spec.agent, command.to_string(), cwd, - ), + ) + .with_route(hook_route_metadata_from_parsed(&parsed, &root)), ) .await; } diff --git a/src/mcp/hook_events.rs b/src/mcp/hook_events.rs index 529d20f9..d78c07bd 100644 --- a/src/mcp/hook_events.rs +++ b/src/mcp/hook_events.rs @@ -28,6 +28,15 @@ impl HookEventKind { _ => None, } } + + pub(crate) fn as_key(self) -> &'static str { + match self { + Self::FileEdit => "file_edit", + Self::Shell => "shell", + Self::WorkspaceOpen => "workspace_open", + Self::IncrementalSync => "incremental_sync", + } + } } pub(crate) struct HookEvent { @@ -36,6 +45,7 @@ pub(crate) struct HookEvent { pub(crate) rel_paths: Vec, pub(crate) command: Option, pub(crate) cwd: Option, + pub(crate) route: Option, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -56,6 +66,7 @@ pub(crate) fn parse_hook_event(params: Option<&Value>) -> Option { rel_paths: safe_hook_rel_paths(&event.rel_paths), command: event.command.filter(|command| !command.is_empty()), cwd: event.cwd, + route: event.route, }) } @@ -214,6 +225,35 @@ mod tests { assert_eq!(workspace.kind, HookEventKind::WorkspaceOpen); } + #[test] + fn preserves_route_metadata_from_hook_notification() { + let params = json!({ + "agent": "codex", + "event": "postToolUseShell", + "command": "cargo test", + "cwd": "/tmp/project", + "route": { + "session_id": "session-123", + "thread_id": "thread-456", + "cwd": "/tmp/project", + "worktree": "/tmp/project-worktree", + "branch": "feature/hook-route" + } + }); + + let event = parse_or_panic(¶ms); + + let route = event.route.as_ref().expect("route metadata should parse"); + assert_eq!(route.session_id.as_deref(), Some("session-123")); + assert_eq!(route.thread_id.as_deref(), Some("thread-456")); + assert_eq!(route.cwd.as_deref(), Some(Path::new("/tmp/project"))); + assert_eq!( + route.worktree.as_deref(), + Some(Path::new("/tmp/project-worktree")) + ); + assert_eq!(route.branch.as_deref(), Some("feature/hook-route")); + } + #[test] fn ignores_unknown_hook_event_names() { let params = json!({ diff --git a/src/mcp/server.rs b/src/mcp/server.rs index 8d265b2f..1460fe10 100644 --- a/src/mcp/server.rs +++ b/src/mcp/server.rs @@ -18,7 +18,9 @@ use crate::global_db::GlobalDb; use crate::mcp::response_handles::{ cleanup_expired_response_handles, response_handle_stats_json, RESPONSE_RETRIEVE_TOOL, }; -use crate::mcp::tool_analytics::{mcp_tool_analytics_event, McpToolAnalyticsEvent}; +use crate::mcp::tool_analytics::{ + hook_route_analytics_event, mcp_tool_analytics_event, McpToolAnalyticsEvent, +}; use crate::path_tree::format_compact_annotated_path_list; use crate::tracedecay::TraceDecay; @@ -1538,6 +1540,7 @@ impl McpServer { let cg = self.reopen_if_branch_drifted().await; let root = cg.project_root().to_path_buf(); let current_branch = crate::branch::current_branch(&root); + self.record_hook_route_analytics(&root, &event, current_branch.as_deref()); let plan = hook_events::plan_hook_event(&event, &root, current_branch.as_deref()); self.run_hook_event_plan(cg, &root, plan).await; } @@ -2236,6 +2239,30 @@ impl McpServer { }); } + fn record_hook_route_analytics( + &self, + project_root: &std::path::Path, + event: &hook_events::HookEvent, + current_branch: Option<&str>, + ) { + let Some(event) = hook_route_analytics_event( + project_root, + event, + current_branch, + crate::tracedecay::current_timestamp(), + ) else { + return; + }; + let Some(gdb) = self.global_db.clone() else { + return; + }; + self.spawn_observed_ledger_write(async move { + if let Err(e) = gdb.append_analytics_event(&event).await { + eprintln!("[tracedecay] hook route analytics insert failed: {e}"); + } + }); + } + fn spawn_observed_ledger_write(&self, future: F) where F: std::future::Future + Send + 'static, diff --git a/src/mcp/tool_analytics.rs b/src/mcp/tool_analytics.rs index 5de9d7c1..24de392d 100644 --- a/src/mcp/tool_analytics.rs +++ b/src/mcp/tool_analytics.rs @@ -1,6 +1,7 @@ use serde_json::{json, Value}; use crate::global_db::{AnalyticsEventInsert, GlobalDb}; +use crate::mcp::hook_events::HookEvent; pub(super) struct McpToolAnalyticsEvent<'a> { pub(super) project_root: &'a std::path::Path, @@ -59,6 +60,42 @@ pub(super) fn mcp_tool_analytics_event(input: McpToolAnalyticsEvent<'_>) -> Anal } } +pub(super) fn hook_route_analytics_event( + project_root: &std::path::Path, + event: &HookEvent, + current_branch: Option<&str>, + timestamp: i64, +) -> Option { + let route = event.route.as_ref()?; + let metadata = json!({ + "agent": event.agent.as_wire(), + "hook_kind": event.kind.as_key(), + "event_cwd": event.cwd.as_ref().map(|path| path.display().to_string()), + "route_cwd": route.cwd.as_ref().map(|path| path.display().to_string()), + "worktree": route.worktree.as_ref().map(|path| path.display().to_string()), + "route_branch": route.branch.as_deref(), + "current_branch": current_branch, + "thread_id": route.thread_id.as_deref(), + "rel_path_count": event.rel_paths.len(), + "has_command": event.command.is_some(), + }); + Some(AnalyticsEventInsert { + provider: "daemon_hook".to_string(), + project_id: GlobalDb::canonical_project_key(project_root), + session_id: route.session_id.clone(), + timestamp, + event_kind: "hook_route".to_string(), + hook_name: Some(event.kind.as_key().to_string()), + tool_name: None, + tool_category: None, + skill_name: None, + hint_category: None, + hint_id: None, + outcome: Some("observed".to_string()), + metadata_json: Some(metadata.to_string()), + }) +} + fn append_tool_response_analytics( tool_name: &str, arguments: &Value, @@ -95,3 +132,48 @@ fn append_tool_response_analytics( "error": null, }); } + +#[cfg(test)] +mod tests { + use std::path::{Path, PathBuf}; + + use crate::daemon::HookRouteMetadata; + use crate::mcp::hook_events::{HookAgent, HookEvent, HookEventKind}; + + use super::hook_route_analytics_event; + + #[test] + fn hook_route_analytics_event_preserves_correlation_fields() { + let event = HookEvent { + agent: HookAgent::Codex, + kind: HookEventKind::Shell, + rel_paths: Vec::new(), + command: Some("cargo test".to_string()), + cwd: Some(PathBuf::from("/repo")), + route: Some(HookRouteMetadata { + session_id: Some("session-123".to_string()), + thread_id: Some("thread-456".to_string()), + cwd: Some(PathBuf::from("/repo")), + worktree: Some(PathBuf::from("/repo")), + branch: Some("feature/hook-route".to_string()), + }), + }; + + let record = hook_route_analytics_event(Path::new("/repo"), &event, Some("main"), 12345) + .expect("route metadata should create analytics record"); + let metadata: serde_json::Value = + serde_json::from_str(record.metadata_json.as_deref().unwrap_or("{}")) + .expect("metadata should parse"); + + assert_eq!(record.provider, "daemon_hook"); + assert_eq!(record.session_id.as_deref(), Some("session-123")); + assert_eq!(record.event_kind, "hook_route"); + assert_eq!(record.hook_name.as_deref(), Some("shell")); + assert_eq!(record.outcome.as_deref(), Some("observed")); + assert_eq!(metadata["agent"], "codex"); + assert_eq!(metadata["thread_id"], "thread-456"); + assert_eq!(metadata["route_branch"], "feature/hook-route"); + assert_eq!(metadata["current_branch"], "main"); + assert_eq!(metadata["has_command"], true); + } +} From 4476d3291132c39e4da0dec3fb96fbe7066e6227 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Thu, 2 Jul 2026 09:40:37 +0000 Subject: [PATCH 2/4] fix(hooks): preserve camelCase route session ids --- src/hooks/mod.rs | 48 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/src/hooks/mod.rs b/src/hooks/mod.rs index 3108caa0..a8bdaa20 100644 --- a/src/hooks/mod.rs +++ b/src/hooks/mod.rs @@ -254,7 +254,7 @@ fn hook_route_metadata_from_parsed( .unwrap_or_else(|| project_root.to_path_buf()); let branch = crate::branch::current_branch(&worktree); Some(crate::daemon::HookRouteMetadata { - session_id: event_session_id(parsed), + session_id: hook_route_session_id(parsed), thread_id: text_field( parsed, &[ @@ -270,6 +270,21 @@ fn hook_route_metadata_from_parsed( }) } +fn hook_route_session_id(parsed: &Value) -> Option { + [ + "session_id", + "sessionId", + "conversation_id", + "conversationId", + "chat_id", + "chatId", + ] + .iter() + .find_map(|key| parsed.get(*key).and_then(Value::as_str)) + .filter(|id| !id.is_empty()) + .map(str::to_string) +} + fn deduped_project_hint( root: Option, agent: HintAgent, @@ -468,3 +483,34 @@ pub(crate) fn read_stdin_to_string() -> std::io::Result { std::io::stdin().read_to_string(&mut input)?; Ok(input) } + +#[cfg(test)] +mod tests { + use super::hook_route_metadata_from_event; + + #[test] + fn hook_route_metadata_preserves_camel_case_session_ids() { + let event = serde_json::json!({ + "sessionId": "session-camel", + "conversationId": "conversation-camel", + "cwd": "/tmp/project" + }) + .to_string(); + + let route = hook_route_metadata_from_event(&event, std::path::Path::new("/tmp/project")) + .expect("route metadata should parse"); + + assert_eq!(route.session_id.as_deref(), Some("session-camel")); + + let event = serde_json::json!({ + "conversationId": "conversation-camel", + "cwd": "/tmp/project" + }) + .to_string(); + + let route = hook_route_metadata_from_event(&event, std::path::Path::new("/tmp/project")) + .expect("route metadata should parse"); + + assert_eq!(route.session_id.as_deref(), Some("conversation-camel")); + } +} From 475d262f8987ebdb31731e011b175a2e03fe71e0 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Thu, 2 Jul 2026 10:05:09 +0000 Subject: [PATCH 3/4] fix(hooks): satisfy clippy for route metadata --- src/daemon.rs | 1 + src/hooks/mod.rs | 22 ++++++++++++++-------- src/hooks/post_tool_use.rs | 4 ++-- src/mcp/hook_events.rs | 4 +++- src/mcp/tool_analytics.rs | 13 +++++++++---- 5 files changed, 29 insertions(+), 15 deletions(-) diff --git a/src/daemon.rs b/src/daemon.rs index bf9d1463..beec0942 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -132,6 +132,7 @@ impl DaemonHookEvent { } } + #[must_use] pub fn with_route(mut self, route: Option) -> Self { self.route = route; self diff --git a/src/hooks/mod.rs b/src/hooks/mod.rs index a8bdaa20..55cd4216 100644 --- a/src/hooks/mod.rs +++ b/src/hooks/mod.rs @@ -241,19 +241,19 @@ fn hook_route_metadata_from_event( project_root: &Path, ) -> Option { let parsed = serde_json::from_str::(event_json).ok()?; - hook_route_metadata_from_parsed(&parsed, project_root) + Some(hook_route_metadata_from_parsed(&parsed, project_root)) } fn hook_route_metadata_from_parsed( parsed: &Value, project_root: &Path, -) -> Option { +) -> crate::daemon::HookRouteMetadata { let cwd = event_cwd_from_parsed(parsed); let route_root = cwd.as_deref().unwrap_or(project_root); let worktree = crate::worktree::git_worktree_root(route_root) .unwrap_or_else(|| project_root.to_path_buf()); let branch = crate::branch::current_branch(&worktree); - Some(crate::daemon::HookRouteMetadata { + crate::daemon::HookRouteMetadata { session_id: hook_route_session_id(parsed), thread_id: text_field( parsed, @@ -267,7 +267,7 @@ fn hook_route_metadata_from_parsed( cwd, worktree: Some(worktree), branch, - }) + } } fn hook_route_session_id(parsed: &Value) -> Option { @@ -497,8 +497,11 @@ mod tests { }) .to_string(); - let route = hook_route_metadata_from_event(&event, std::path::Path::new("/tmp/project")) - .expect("route metadata should parse"); + let Some(route) = + hook_route_metadata_from_event(&event, std::path::Path::new("/tmp/project")) + else { + panic!("route metadata should parse"); + }; assert_eq!(route.session_id.as_deref(), Some("session-camel")); @@ -508,8 +511,11 @@ mod tests { }) .to_string(); - let route = hook_route_metadata_from_event(&event, std::path::Path::new("/tmp/project")) - .expect("route metadata should parse"); + let Some(route) = + hook_route_metadata_from_event(&event, std::path::Path::new("/tmp/project")) + else { + panic!("route metadata should parse"); + }; assert_eq!(route.session_id.as_deref(), Some("conversation-camel")); } diff --git a/src/hooks/post_tool_use.rs b/src/hooks/post_tool_use.rs index 8e66aea2..b135af11 100644 --- a/src/hooks/post_tool_use.rs +++ b/src/hooks/post_tool_use.rs @@ -87,7 +87,7 @@ pub(crate) async fn notify_post_tool_use(spec: &PostToolUseSpec, event_json: &st crate::daemon::notify_hook_event( &root, crate::daemon::DaemonHookEvent::post_tool_use_edit(spec.agent, rels, cwd) - .with_route(hook_route_metadata_from_parsed(&parsed, &root)), + .with_route(Some(hook_route_metadata_from_parsed(&parsed, &root))), ) .await; } else if (spec.is_shell_tool)(tool_name) { @@ -102,7 +102,7 @@ pub(crate) async fn notify_post_tool_use(spec: &PostToolUseSpec, event_json: &st command.to_string(), cwd, ) - .with_route(hook_route_metadata_from_parsed(&parsed, &root)), + .with_route(Some(hook_route_metadata_from_parsed(&parsed, &root))), ) .await; } diff --git a/src/mcp/hook_events.rs b/src/mcp/hook_events.rs index d78c07bd..930d58f6 100644 --- a/src/mcp/hook_events.rs +++ b/src/mcp/hook_events.rs @@ -243,7 +243,9 @@ mod tests { let event = parse_or_panic(¶ms); - let route = event.route.as_ref().expect("route metadata should parse"); + let Some(route) = event.route.as_ref() else { + panic!("route metadata should parse"); + }; assert_eq!(route.session_id.as_deref(), Some("session-123")); assert_eq!(route.thread_id.as_deref(), Some("thread-456")); assert_eq!(route.cwd.as_deref(), Some(Path::new("/tmp/project"))); diff --git a/src/mcp/tool_analytics.rs b/src/mcp/tool_analytics.rs index 24de392d..af47ebe1 100644 --- a/src/mcp/tool_analytics.rs +++ b/src/mcp/tool_analytics.rs @@ -159,11 +159,16 @@ mod tests { }), }; - let record = hook_route_analytics_event(Path::new("/repo"), &event, Some("main"), 12345) - .expect("route metadata should create analytics record"); + let Some(record) = + hook_route_analytics_event(Path::new("/repo"), &event, Some("main"), 12345) + else { + panic!("route metadata should create analytics record"); + }; let metadata: serde_json::Value = - serde_json::from_str(record.metadata_json.as_deref().unwrap_or("{}")) - .expect("metadata should parse"); + match serde_json::from_str(record.metadata_json.as_deref().unwrap_or("{}")) { + Ok(metadata) => metadata, + Err(err) => panic!("metadata should parse: {err}"), + }; assert_eq!(record.provider, "daemon_hook"); assert_eq!(record.session_id.as_deref(), Some("session-123")); From 73627904bbda888669cdc619c8ec92d1e85ff00f Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Thu, 2 Jul 2026 16:37:58 +0000 Subject: [PATCH 4/4] refactor(hooks): reuse route text field helper --- src/hooks/mod.rs | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/hooks/mod.rs b/src/hooks/mod.rs index 55cd4216..f55f42b5 100644 --- a/src/hooks/mod.rs +++ b/src/hooks/mod.rs @@ -271,18 +271,17 @@ fn hook_route_metadata_from_parsed( } fn hook_route_session_id(parsed: &Value) -> Option { - [ - "session_id", - "sessionId", - "conversation_id", - "conversationId", - "chat_id", - "chatId", - ] - .iter() - .find_map(|key| parsed.get(*key).and_then(Value::as_str)) - .filter(|id| !id.is_empty()) - .map(str::to_string) + text_field( + parsed, + &[ + "session_id", + "sessionId", + "conversation_id", + "conversationId", + "chat_id", + "chatId", + ], + ) } fn deduped_project_hint(