Records a live session↔git span from one hook route notification. Route metadata carries `(session_id, thread_id, cwd, worktree, branch)`; when the route names a session and resolves to a registered project, this folds one [`SpanObservation`] into that project's `sessions.db` span table (see [`crate::sessions::git_correlation`]). Mid-session branch/worktree switches are handled by the span table
(&self, event: &hook_events::HookEvent)
| 2774 | /// merge regardless, so a dropped observation only widens a span slightly |
| 2775 | /// less). |
| 2776 | async fn record_hook_span_observation(&self, event: &hook_events::HookEvent) { |
| 2777 | use crate::sessions::git_correlation::{ |
| 2778 | self as gc, SpanObservation, SpanSource, DEFAULT_SPAN_MERGE_GAP_SECS, |
| 2779 | DEFAULT_SPAN_OBSERVATION_DEBOUNCE_SECS, |
| 2780 | }; |
| 2781 | |
| 2782 | let Some(route) = event.route.as_ref() else { |
| 2783 | return; |
| 2784 | }; |
| 2785 | let Some(session_id) = route |
| 2786 | .session_id |
| 2787 | .as_deref() |
| 2788 | .map(str::trim) |
| 2789 | .filter(|value| !value.is_empty()) |
| 2790 | else { |
| 2791 | return; |
| 2792 | }; |
| 2793 | // Resolve the project for this route the same way route caching does; |
| 2794 | // spans belong to that project's store, not this server's checkout. |
| 2795 | let route_cwd = route.cwd.as_deref().or(event.cwd.as_deref()); |
| 2796 | let Some(cwd) = route_cwd else { |
| 2797 | return; |
| 2798 | }; |
| 2799 | let Some(project_root) = self.registered_project_for_route_cwd(cwd).await else { |
| 2800 | return; |
| 2801 | }; |
| 2802 | let project_root = PathBuf::from(project_root); |
| 2803 | |
| 2804 | // Worktree preference: the routed worktree, else the cwd's git |
| 2805 | // worktree root, else the resolved project root. Never fabricated. |
| 2806 | let worktree_raw = route |
| 2807 | .worktree |
| 2808 | .as_deref() |
| 2809 | .map(Path::to_path_buf) |
| 2810 | .or_else(|| crate::worktree::git_worktree_root(cwd)) |
| 2811 | .unwrap_or_else(|| project_root.clone()); |
| 2812 | let worktree = gc::normalize_worktree(&worktree_raw.to_string_lossy()); |
| 2813 | |
| 2814 | let branch = route |
| 2815 | .branch |
| 2816 | .as_deref() |
| 2817 | .map(str::trim) |
| 2818 | .filter(|value| !value.is_empty()) |
| 2819 | .map(str::to_string); |
| 2820 | let thread_id = route |
| 2821 | .thread_id |
| 2822 | .as_deref() |
| 2823 | .map(str::trim) |
| 2824 | .filter(|value| !value.is_empty()) |
| 2825 | .map(str::to_string); |
| 2826 | let ts = crate::tracedecay::current_timestamp(); |
| 2827 | |
| 2828 | // Hook routes are provider-agnostic: leave provider empty. |
| 2829 | let key = gc::span_debounce_key("", session_id, branch.as_deref(), &worktree); |
| 2830 | let should_record = self |
| 2831 | .span_observation_debounce |
| 2832 | .lock() |
| 2833 | .map_or(true, |mut debounce| { |
no test coverage detected