From 6645ea0a4775e334729c00646dddd1d8108845db Mon Sep 17 00:00:00 2001 From: Hare Date: Sat, 17 Oct 2026 00:10:35 +0900 Subject: [PATCH] fix: observe original presence through saved legacy Journal links --- .../src/journal/memory_links/tests.rs | 58 +++++++++++++++++++ crates/subjektiv/src/journal/source_status.rs | 16 ++++- 2 files changed, 73 insertions(+), 1 deletion(-) diff --git a/crates/subjektiv/src/journal/memory_links/tests.rs b/crates/subjektiv/src/journal/memory_links/tests.rs index 439a111b3..d2d6e639c 100644 --- a/crates/subjektiv/src/journal/memory_links/tests.rs +++ b/crates/subjektiv/src/journal/memory_links/tests.rs @@ -404,6 +404,64 @@ fn original_source_metadata_failure_keeps_saved_journal_readable_and_does_not_cl } } +#[test] +fn legacy_segment_entry_links_reuse_saved_session_for_presence_without_mutating_provenance() { + let mut f = Fixture::new(); + let mut candidate = f.candidate("legacy-presence", &[0, 1]); + for source in &mut candidate.source_refs { + source.session_id = None; + } + let candidate = f.store().stage_candidate(candidate).unwrap(); + let selector = f.apply(&candidate); + let entry = f.fixed("first", 1, vec![]); + f.links(&selector, true); + let before = f.snapshot(); + f.reopen(); + for request in [ + MemoryJournalRequest::Links { + selector: selector.clone(), + }, + MemoryJournalRequest::Read { + selector: selector.clone(), + source_index: 1, + journal_id: f.journal_id.clone(), + entry_id: entry.clone(), + expected_token: None, + byte_offset: 0, + max_bytes: 4096, + }, + ] { + let mut calls = Vec::new(); + let response = f + .store() + .execute_memory_journal_with_source_presence(&f.host, request, |session| { + calls.push(session.to_owned()); + Some(false) + }) + .unwrap(); + assert_eq!(calls, ["session"]); + match response { + MemoryJournalResponse::Links(page) => { + assert_eq!(page.sources.len(), 2); + assert!( + page.sources + .iter() + .all(|s| s.original_source == OriginalSourceAvailability::Missing) + ); + } + MemoryJournalResponse::Read(read) => { + assert_eq!( + read.source.original_source, + OriginalSourceAvailability::Missing + ); + assert_eq!(read.page.unwrap().text, "Journal context for first"); + } + _ => panic!("saved provenance result required"), + } + assert_eq!(f.snapshot(), before); + } +} + #[test] fn multiple_segment_anchors_map_to_fixed_intervals_and_subject_crossing_is_denied() { let f = Fixture::new(); diff --git a/crates/subjektiv/src/journal/source_status.rs b/crates/subjektiv/src/journal/source_status.rs index 58e5f322e..f60959829 100644 --- a/crates/subjektiv/src/journal/source_status.rs +++ b/crates/subjektiv/src/journal/source_status.rs @@ -7,6 +7,7 @@ use super::memory_links::{ MemoryJournalRequest, MemoryJournalResponse, OriginalSourceAvailability, }; use super::{JournalHostContext, JournalResult, SubjektivStore}; +use rusqlite::{OptionalExtension, params}; impl SubjektivStore { /// Executes the saved-reference operation first, then annotates original-source absence using @@ -39,7 +40,20 @@ impl SubjektivStore { let source = candidate .as_ref() .and_then(|c| c.source_refs.get(link.source_index)); - let session = source.and_then(|s| s.session_id.as_deref()); + // Legacy anchors may lack a Session ID. A connected edge already resolved it + // from immutable attribution and stable Segment/entry identity; reuse that saved + // identity rather than fetching history or modifying the original candidate. + let saved_session: Option = self.database.try_with_connection(|tx| { + Ok::<_, super::JournalError>(tx.query_row( + "SELECT source_session_id FROM memory_journal_links + WHERE subject_id=?1 AND candidate_id=?2 AND source_index=?3 AND reason='linked'", + params![host.subject_id, selector.candidate_id, link.source_index], + |row| row.get(0), + ).optional()?) + })?; + let session = saved_session + .as_deref() + .or_else(|| source.and_then(|s| s.session_id.as_deref())); let observed = if let Some(session) = session { if let Some(value) = observations.get(session) { *value