From 6776973e4c3960ee4e072a963259ff87ee6fc065 Mon Sep 17 00:00:00 2001 From: Hare Date: Thu, 22 Oct 2026 23:17:25 +0900 Subject: [PATCH] fix: preserve original session coordinates through capture selection --- crates/session-store/src/capture.rs | 50 ++++--- crates/session-store/src/capture/body.rs | 52 +++++++- .../session-store/tests/capture_contract.rs | 125 +++++++++++++++++- .../src/feature/builtin/flow_transition.rs | 2 +- .../src/feature/builtin/memory_lifecycle.rs | 99 +++++++++++++- .../feature/builtin/memory_staging_output.rs | 49 +++++++ .../src/feature/builtin/subjektiv_memory.rs | 6 +- crates/worker/src/session_capture.rs | 15 ++- crates/worker/src/worker.rs | 22 ++- 9 files changed, 368 insertions(+), 52 deletions(-) diff --git a/crates/session-store/src/capture.rs b/crates/session-store/src/capture.rs index 4c9b4d3bc..07da4c1f7 100644 --- a/crates/session-store/src/capture.rs +++ b/crates/session-store/src/capture.rs @@ -237,6 +237,7 @@ pub struct SessionEntryEvidence { pub segment_id: String, pub entry_ref: SessionEntryRef, pub origin: SessionEntryProvenance, + /// Inclusive original-source positions in `segment_id`, not view/UI order. pub entry_range: [u64; 2], pub kind: ReferenceKind, pub tool_part: Option, @@ -259,6 +260,7 @@ pub struct SessionCapture { session_id: String, segment_id: String, capture_id: String, + view_entry_count: u64, entries: Arc>, overview: Vec, index: Vec, @@ -271,6 +273,7 @@ impl SessionCapture { snapshot: SessionSnapshot, ) -> Self { let segment_id = segment_id.into(); + let view_entry_count = snapshot.entries.len() as u64; let entries = snapshot .entries .into_iter() @@ -341,7 +344,7 @@ impl SessionCapture { }) }) .collect(); - Self::from_projected(session_id.into(), segment_id, entries) + Self::from_projected(session_id.into(), segment_id, view_entry_count, entries) } pub fn new( @@ -350,6 +353,7 @@ impl SessionCapture { items: Vec, ) -> Self { let segment_id = segment_id.into(); + let view_entry_count = items.len() as u64; let entries = items .into_iter() .enumerate() @@ -371,15 +375,19 @@ impl SessionCapture { }) }) .collect(); - Self::from_projected(session_id.into(), segment_id, entries) + Self::from_projected(session_id.into(), segment_id, view_entry_count, entries) } - pub fn from_history_entries( + /// Project a complete Host-fixed history whose source positions start at zero. + /// Select a suffix with `select_order_range` on the resulting value instead + /// of passing a rebased history vector: annotations do not carry source positions. + pub fn from_complete_history( session_id: impl Into, segment_id: impl Into, entries: Vec>, ) -> Self { let segment_id = segment_id.into(); + let view_entry_count = entries.len() as u64; let entries = entries .into_iter() .enumerate() @@ -432,14 +440,20 @@ impl SessionCapture { }) }) .collect(); - Self::from_projected(session_id.into(), segment_id, entries) + Self::from_projected(session_id.into(), segment_id, view_entry_count, entries) } - fn from_projected(session_id: String, segment_id: String, dtos: Vec) -> Self { + fn from_projected( + session_id: String, + segment_id: String, + view_entry_count: u64, + dtos: Vec, + ) -> Self { let dto = CaptureDto { version: CAPTURE_VERSION, session_id: session_id.clone(), segment_id: segment_id.clone(), + view_entry_count, entries: dtos, }; let capture_id = dto.fingerprint(); @@ -589,6 +603,7 @@ impl SessionCapture { session_id, segment_id, capture_id, + view_entry_count, entries: Arc::new(entries), overview, index, @@ -604,7 +619,8 @@ impl SessionCapture { &self.overview } - pub fn source_index_for_ref(&self, reference: &SessionEntryRef) -> Option { + /// Capture-local UI position, not the original source position used by evidence. + pub fn view_order_for_ref(&self, reference: &SessionEntryRef) -> Option { self.index .iter() .find(|entry| entry.id == *reference) @@ -624,12 +640,12 @@ impl SessionCapture { let min_entry_index = options .from .as_ref() - .and_then(|reference| self.source_index_for_ref(reference)) + .and_then(|reference| self.view_order_for_ref(reference)) .unwrap_or_else(|| options.min_entry_index.unwrap_or(0)); let max_entry_index = options .through .as_ref() - .and_then(|reference| self.source_index_for_ref(reference)) + .and_then(|reference| self.view_order_for_ref(reference)) .unwrap_or(u64::MAX); let mut skipped = 0usize; let mut hits = Vec::new(); @@ -787,24 +803,16 @@ impl SessionCapture { if entry.use_policy.is_some_and(|p| !p.memory_evidence) { return None; } - let item = &self + let captured = self .entries .iter() - .find(|captured| captured.dto.order == entry.entry_range[0])? - .item; - let excerpt = render_item(item, entry, ReadDetail::Compact, 2 * 1024); + .find(|captured| captured.dto.order == entry.entry_range[0])?; + let excerpt = render_item(&captured.item, entry, ReadDetail::Compact, 2 * 1024); Some(SessionEntryEvidence { - segment_id: self - .entries - .iter() - .find(|captured| captured.dto.order == entry.entry_range[0])? - .dto - .source - .segment_id - .clone(), + segment_id: captured.dto.source.segment_id.clone(), entry_ref: entry.id.clone(), origin: entry.origin.clone(), - entry_range: entry.entry_range, + entry_range: [captured.dto.source.entry_index; 2], kind: entry.kind, tool_part: entry.tool_part, label: entry.label.clone(), diff --git a/crates/session-store/src/capture/body.rs b/crates/session-store/src/capture/body.rs index 747586889..eb7768622 100644 --- a/crates/session-store/src/capture/body.rs +++ b/crates/session-store/src/capture/body.rs @@ -307,6 +307,8 @@ pub struct CaptureDto { pub version: u32, pub session_id: String, pub segment_id: String, + /// Extent of view order, including positions omitted by public projection. + pub view_entry_count: u64, pub entries: Vec, } @@ -409,6 +411,9 @@ impl CaptureDto { { return Err(CaptureError::Invalid("entry identity")); } + if entry.order >= self.view_entry_count { + return Err(CaptureError::Invalid("entry outside view extent")); + } if previous.is_some_and(|order| order >= entry.order) { return Err(CaptureError::Invalid("entry order")); } @@ -598,6 +603,7 @@ impl SessionCapture { version: CAPTURE_VERSION, session_id: self.session_id.clone(), segment_id: self.segment_id.clone(), + view_entry_count: self.view_entry_count, entries: self.entries.iter().map(|e| e.dto.clone()).collect(), } } @@ -607,6 +613,7 @@ impl SessionCapture { Ok(Self::from_projected( dto.session_id, dto.segment_id, + dto.view_entry_count, dto.entries, )) } @@ -618,11 +625,48 @@ impl SessionCapture { Self::try_from_dto(CaptureDto::decode(bytes, limits)?, limits) } + /// Select fixed view positions. UI order is rebased, while every original + /// source identity/position, body, provenance and policy stays unchanged. + pub fn select_order_range(&self, range: std::ops::Range) -> Result { + if range.start > range.end || range.end > self.view_entry_count { + return Err(CaptureError::InvalidRange); + } + let entries = self + .entries() + .filter(|entry| range.contains(&entry.order)) + .map(|entry| { + let mut entry = entry.clone(); + entry.order -= range.start; + entry + }) + .collect(); + Ok(Self::from_projected( + self.session_id.clone(), + self.segment_id.clone(), + range.end - range.start, + entries, + )) + } + + /// Host-owned target selection may exclude entries without renumbering either + /// the fixed view or its original sources. No source I/O or business policy is applied. + pub fn retain_entries(&self, mut include: impl FnMut(&CaptureEntryDto) -> bool) -> Self { + Self::from_projected( + self.session_id.clone(), + self.segment_id.clone(), + self.view_entry_count, + self.entries() + .filter(|entry| include(entry)) + .cloned() + .collect(), + ) + } + pub fn scoped_ref( &self, reference: &SessionEntryRef, ) -> Result { - if self.source_index_for_ref(reference).is_none() { + if self.view_order_for_ref(reference).is_none() { return Err(CaptureError::UnknownReference); } Ok(ScopedSessionEntryRef { @@ -679,7 +723,7 @@ impl SessionCapture { .from .as_ref() .map(|r| { - self.source_index_for_ref(r) + self.view_order_for_ref(r) .ok_or(CaptureError::UnknownReference) }) .transpose()?; @@ -687,7 +731,7 @@ impl SessionCapture { .through .as_ref() .map(|r| { - self.source_index_for_ref(r) + self.view_order_for_ref(r) .ok_or(CaptureError::UnknownReference) }) .transpose()?; @@ -710,7 +754,7 @@ impl SessionCapture { match selector { ReadSelector::Id(id) => { let reference = SessionEntryRef::parse(id).ok_or(CaptureError::UnknownReference)?; - self.source_index_for_ref(&reference) + self.view_order_for_ref(&reference) .ok_or(CaptureError::UnknownReference)?; } ReadSelector::EntryRange([start, end]) => { diff --git a/crates/session-store/tests/capture_contract.rs b/crates/session-store/tests/capture_contract.rs index a605d6d6e..6925fb9df 100644 --- a/crates/session-store/tests/capture_contract.rs +++ b/crates/session-store/tests/capture_contract.rs @@ -77,7 +77,7 @@ fn public_consumer_roundtrip_retains_body_original_source_order_origin_and_indep Some(HistoryInputMetadata::new(usage(false, false, true), None, None).unwrap()), ), ]; - let capture = SessionCapture::from_history_entries("synthetic-session", "selected", entries); + let capture = SessionCapture::from_complete_history("synthetic-session", "selected", entries); let mut dto = capture.to_dto(); // The checked boundary also accepts entries retained from different original segments. dto.entries[0].source.segment_id = "original-a".into(); @@ -115,6 +115,7 @@ fn public_consumer_roundtrip_retains_body_original_source_order_origin_and_indep assert!(restored.evidence_for("Erecording").is_none()); let evidence = restored.evidence_for("Ememory").unwrap(); assert_eq!(evidence.segment_id, "original-b"); + assert_eq!(evidence.entry_range, [3, 3]); assert!(evidence.excerpt.contains("memory-only")); let recorded = restored .slice(CapturePurpose::Recording, None, 10, 16_000) @@ -159,7 +160,7 @@ fn common_data_drops_reasoning_raw_system_and_private_payload_even_when_unindexe HistoryInputMetadata::new(usage(true, true, true), Some(payload.clone()), Some(public)) .unwrap(); let private = HistoryInputMetadata::new(usage(true, true, true), Some(payload), None).unwrap(); - let capture = SessionCapture::from_history_entries( + let capture = SessionCapture::from_complete_history( "s", "segment", vec![ @@ -195,7 +196,7 @@ fn common_data_drops_reasoning_raw_system_and_private_payload_even_when_unindexe // Filtered entries do not renumber the public source positions. assert_eq!(capture.to_dto().entries[0].order, 3); assert_eq!( - capture.source_index_for_ref(&SessionEntryRef::parse("Eordinary").unwrap()), + capture.view_order_for_ref(&SessionEntryRef::parse("Eordinary").unwrap()), Some(4) ); } @@ -318,6 +319,7 @@ fn malformed_dtos_and_oversized_representations_are_rejected() { .is_err() ); let mut reversed = capture.to_dto(); + reversed.view_entry_count = 2; let mut second = reversed.entries[0].clone(); second.entry_id = "second".into(); second.source.entry_index = 4; @@ -325,6 +327,16 @@ fn malformed_dtos_and_oversized_representations_are_rejected() { reversed.entries[0].order = 1; reversed.entries.push(second); assert!(SessionCapture::try_from_dto(reversed, CaptureLimits::default()).is_err()); + let mut duplicate_source = capture.to_dto(); + duplicate_source.view_entry_count = 2; + let mut second = duplicate_source.entries[0].clone(); + second.entry_id = "distinct-id".into(); + second.order = 1; + duplicate_source.entries.push(second); + assert!(matches!( + SessionCapture::try_from_dto(duplicate_source, CaptureLimits::default()), + Err(CaptureError::Invalid("duplicate source position")) + )); } #[test] @@ -503,7 +515,7 @@ fn snapshot_immutability_and_scoped_operations_reject_foreign_or_unknown_refs_an 2 ); let unknown = SessionEntryRef::parse("E99999999").unwrap(); - assert_eq!(capture.source_index_for_ref(&unknown), None); + assert_eq!(capture.view_order_for_ref(&unknown), None); assert!(matches!( capture.scoped_ref(&unknown), Err(CaptureError::UnknownReference) @@ -592,3 +604,108 @@ fn slices_preserve_empty_and_long_result_fields_under_item_and_metadata_budgets( Err(CaptureError::BudgetTooSmall) )); } + +#[test] +fn evidence_uses_original_segment_positions_after_roundtrip_and_view_selection() { + let full = SessionCapture::new( + "s", + "selected", + vec![ + Item::user_message("first"), + Item::assistant_message("second"), + ], + ); + let mut dto = full.to_dto(); + dto.entries[0].source.segment_id = "original-a".into(); + dto.entries[0].source.entry_index = 80; + dto.entries[1].source.segment_id = "original-b".into(); + dto.entries[1].source.entry_index = 3; + let encoded = dto.encode(CaptureLimits::default()).unwrap(); + let restored = SessionCapture::decode(&encoded, CaptureLimits::default()).unwrap(); + for (id, segment, index, order) in [ + ("E00000000", "original-a", 80, 0), + ("E00000001", "original-b", 3, 1), + ] { + let evidence = restored.evidence_for(id).unwrap(); + assert_eq!(evidence.segment_id, segment); + assert_eq!(evidence.entry_range, [index; 2]); + let read = restored.read(ReadSelector::Id(id), ReadOptions::default()); + assert_eq!(read.entries[0].entry_range, [order; 2]); + } + let selected = restored.select_order_range(1..2).unwrap(); + assert_eq!(selected.overview()[0].entry_range, [0, 0]); + let evidence = selected.evidence_for("E00000001").unwrap(); + assert_eq!(evidence.segment_id, "original-b"); + assert_eq!(evidence.entry_range, [3, 3]); + let roundtrip = SessionCapture::decode( + &selected.encode(CaptureLimits::default()).unwrap(), + CaptureLimits::default(), + ) + .unwrap(); + assert_eq!(roundtrip.to_dto(), selected.to_dto()); + assert_eq!( + roundtrip.evidence_for("E00000001").unwrap().entry_range, + [3, 3] + ); +} + +#[test] +fn fixed_history_selection_preserves_original_coordinates_and_checks_private_position_bounds() { + let history = vec![ + history("old", Item::user_message("old"), None), + history("hidden", Item::reasoning("PRIVATE"), None), + history("selected", Item::user_message("selected"), None), + history("private-tail", Item::system_message("PRIVATE"), None), + ]; + let full = SessionCapture::from_complete_history("s", "seg", history); + let suffix = full.select_order_range(1..4).unwrap(); + let original = full + .entries() + .find(|entry| entry.entry_id == "selected") + .unwrap(); + let selected = suffix.entries().next().unwrap(); + assert_eq!(selected.entry_id, original.entry_id); + assert_eq!(selected.source, original.source); + assert_eq!(selected.body, original.body); + assert_eq!(selected.policy, original.policy); + assert!(suffix.evidence_for("Eold").is_none()); + assert_eq!(suffix.search(&SearchOptions::default()).len(), 1); + assert_eq!( + suffix.view_order_for_ref(&SessionEntryRef::parse("Eselected").unwrap()), + Some(1) + ); + assert_eq!(selected.source.entry_index, 2); + assert_eq!(selected.order, 1); + assert_eq!(suffix.to_dto().view_entry_count, 3); + assert_eq!( + suffix.evidence_for("Eselected").unwrap().entry_range, + [2, 2] + ); + let nested = suffix.select_order_range(1..2).unwrap(); + assert_eq!(nested.entries().next().unwrap().order, 0); + assert_eq!(nested.entries().next().unwrap().source, original.source); + let restored = SessionCapture::decode( + &suffix.encode(CaptureLimits::default()).unwrap(), + CaptureLimits::default(), + ) + .unwrap(); + assert_eq!(restored.to_dto(), suffix.to_dto()); + assert!(full.select_order_range(4..5).is_err()); + let reversed = std::ops::Range { start: 3, end: 2 }; + assert!(full.select_order_range(reversed).is_err()); + let hidden_only = full.select_order_range(3..4).unwrap(); + assert_eq!(hidden_only.entries().len(), 0); + assert_eq!(hidden_only.to_dto().view_entry_count, 1); + let mut invalid = full.to_dto(); + invalid.view_entry_count = 2; // the selected entry's view order is outside this extent + assert!(SessionCapture::try_from_dto(invalid, CaptureLimits::default()).is_err()); + assert_eq!( + full.retain_entries(|entry| entry.entry_id == "selected") + .entries() + .next() + .unwrap() + .source, + original.source + ); + assert_eq!(full.to_dto().view_entry_count, 4); +} diff --git a/crates/worker/src/feature/builtin/flow_transition.rs b/crates/worker/src/feature/builtin/flow_transition.rs index 4c9cd98e3..39115d664 100644 --- a/crates/worker/src/feature/builtin/flow_transition.rs +++ b/crates/worker/src/feature/builtin/flow_transition.rs @@ -474,7 +474,7 @@ where self.segment_id, &entries, )?; - Ok(SessionCapture::from_history_entries( + Ok(SessionCapture::from_complete_history( self.session_id.to_string(), self.segment_id.to_string(), history, diff --git a/crates/worker/src/feature/builtin/memory_lifecycle.rs b/crates/worker/src/feature/builtin/memory_lifecycle.rs index 7db770eb0..7af3b867e 100644 --- a/crates/worker/src/feature/builtin/memory_lifecycle.rs +++ b/crates/worker/src/feature/builtin/memory_lifecycle.rs @@ -456,11 +456,13 @@ impl MemoryLifecycleTask { .await; return Ok(()); } - let view = SessionCapture::from_history_entries( + let view = SessionCapture::from_complete_history( capture.session_id.clone(), capture.segment_id.clone(), - capture.history[history_start..history_end].to_vec(), - ); + capture.history.clone(), + ) + .select_order_range(history_start as u64..history_end as u64) + .map_err(hook_internal)?; let start_entry = pointer .as_ref() .map(|pointer| pointer.processed_through_entry + 1) @@ -1557,7 +1559,7 @@ permission = "write" #[tokio::test] async fn run_committed_background_task_stages_non_empty_extraction_and_commits_pointer() { let source = capture(2, 250); - let entry_ref = SessionCapture::from_history_entries( + let entry_ref = SessionCapture::from_complete_history( source.session_id.clone(), source.segment_id.clone(), source.history.clone(), @@ -1610,10 +1612,95 @@ permission = "write" ); } + // Real background extraction -> common fixed selection -> StageMemoryCandidate + // -> typed backend operation and pointer write. Only model/backend transport + // are replaced; the second extraction uses the first run's saved pointer. + #[tokio::test] + async fn second_extraction_preserves_original_evidence_positions_after_nonzero_pointer() { + let first = capture(2, 250); + let mut second = capture(4, 750); + second.history[..2].clone_from_slice(&first.history); + second + .usage_history + .insert(0, first.usage_history[0].clone()); + let selected_ref = + crate::SessionEntryRef::from_history_entry_id(&second.history[2].annotation.entry_id) + .to_string(); + let extension_writes = Arc::new(Mutex::new(Vec::new())); + let workspace_client = Arc::new(RecordingWorkspaceClient::default()); + let (event_tx, _) = broadcast::channel(64); + run_background_task(test_task( + first, + Box::new(ScriptClient::new(vec![ + finish_empty_events("first-empty"), + completed_events(), + ])), + extension_writes.clone(), + event_tx.clone(), + workspace_client.clone(), + )) + .await; + { + let writes = extension_writes.lock().unwrap(); + assert_eq!(writes.len(), 1); + let pointer: memory::ExtractPointerPayload = + serde_json::from_value(writes[0].2.clone()).unwrap(); + assert_eq!(pointer.processed_through_history_len, 2); + second + .extensions + .push((writes[0].1.clone(), writes[0].2.clone())); + } + run_background_task(test_task( + second, + Box::new(ScriptClient::new(vec![ + stage_candidate_events("second-stage", &selected_ref), + finish_events("second-finish", 1), + completed_events(), + ])), + extension_writes.clone(), + event_tx, + workspace_client.clone(), + )) + .await; + let operations = workspace_client + .requests + .lock() + .unwrap() + .iter() + .filter_map(|request| { + request.body.as_deref().and_then(|body| { + serde_json::from_str::(body).ok() + }) + }) + .filter_map(|operation| match operation { + memory::backend::MemoryBackendOperation::StageCandidate(operation) => { + Some(operation) + } + _ => None, + }) + .collect::>(); + assert_eq!(operations.len(), 1); + let operation = &operations[0]; + assert_eq!(operation.source.range, [2, 3]); + assert_eq!(operation.evidence[0].id, selected_ref); + assert_eq!(operation.evidence[0].entry_range, Some([2, 2])); + assert_eq!( + operation.source_refs[0].segment_id.as_deref(), + Some("segment-1") + ); + assert_eq!(operation.source_refs[0].entry_range, Some([2, 2])); + let writes = extension_writes.lock().unwrap(); + assert_eq!(writes.len(), 2); + let pointer: memory::ExtractPointerPayload = + serde_json::from_value(writes[1].2.clone()).unwrap(); + assert_eq!(pointer.processed_through_history_len, 4); + assert_eq!(pointer.processed_through_entry, 3); + } + #[tokio::test] async fn subjektiv_extraction_stages_once_and_commits_its_independent_pointer() { let source = capture(2, 250); - let entry_ref = SessionCapture::from_history_entries( + let entry_ref = SessionCapture::from_complete_history( source.session_id.clone(), source.segment_id.clone(), source.history.clone(), @@ -1722,7 +1809,7 @@ permission = "write" #[tokio::test] async fn subjektiv_candidate_remains_staged_when_later_extraction_fails_without_pointer() { let source = capture(2, 250); - let entry_ref = SessionCapture::from_history_entries( + let entry_ref = SessionCapture::from_complete_history( source.session_id.clone(), source.segment_id.clone(), source.history.clone(), diff --git a/crates/worker/src/feature/builtin/memory_staging_output.rs b/crates/worker/src/feature/builtin/memory_staging_output.rs index dd830349d..2ac95d8bf 100644 --- a/crates/worker/src/feature/builtin/memory_staging_output.rs +++ b/crates/worker/src/feature/builtin/memory_staging_output.rs @@ -639,4 +639,53 @@ mod tests { .unwrap_err(); assert!(format!("{error:?}").contains("unknown SessionEntryRef")); } + #[tokio::test] + async fn staging_roundtripped_mixed_sources_uses_original_evidence_ranges_not_view_order() { + let client = Arc::new(RecordingSubjektivClient::default()); + let mut dto = SessionCapture::new( + "session", + "view", + vec![ + Item::user_message("first"), + Item::assistant_message("second"), + ], + ) + .to_dto(); + dto.entries[0].source.segment_id = "original-a".into(); + dto.entries[0].source.entry_index = 80; + dto.entries[1].source.segment_id = "original-b".into(); + dto.entries[1].source.entry_index = 3; + let limits = crate::session_capture::CaptureLimits::default(); + let view = SessionCapture::decode(&dto.encode(limits).unwrap(), limits).unwrap(); + let state = MemoryStagingOutputState::new_subjektiv( + view, + crate::subjektiv::test_connection(client.clone()), + SourceRef { + segment_id: "view".into(), + range: [0, 1], + }, + "run".into(), + ); + StageMemoryCandidateTool { state }.execute( + r#"{"kind":"decision","claim":"claim","why_useful":"useful","entry_refs":["E00000000","E00000001"]}"#, + agen::tool::ToolExecutionContext::direct(), + ).await.unwrap(); + let requests = client.requests.lock().unwrap(); + let request: server_api::SubjektivStageCandidateRequest = + serde_json::from_str(requests[0].body.as_deref().unwrap()).unwrap(); + for (index, segment, position) in [(0, "original-a", 80), (1, "original-b", 3)] { + assert_eq!( + request.operation.evidence[index].entry_range, + Some([position; 2]) + ); + assert_eq!( + request.operation.source_refs[index].segment_id.as_deref(), + Some(segment) + ); + assert_eq!( + request.operation.source_refs[index].entry_range, + Some([position; 2]) + ); + } + } } diff --git a/crates/worker/src/feature/builtin/subjektiv_memory.rs b/crates/worker/src/feature/builtin/subjektiv_memory.rs index 82ce970f9..9b5f3c387 100644 --- a/crates/worker/src/feature/builtin/subjektiv_memory.rs +++ b/crates/worker/src/feature/builtin/subjektiv_memory.rs @@ -571,7 +571,7 @@ impl SubjektivMemoryState { .capture() .map_err(|error| ToolError::ExecutionFailed(error.to_string()))?; let session_id = capture.session_id.clone(); - let view = SessionCapture::from_history_entries( + let view = SessionCapture::from_complete_history( capture.session_id.clone(), capture.segment_id, capture.history, @@ -680,7 +680,7 @@ fn replay_pending_committed_memory_with_policy( .capture .capture() .map_err(|error| HookError::new(HookErrorCategory::Dependency, error.to_string()))?; - let view = SessionCapture::from_history_entries( + let view = SessionCapture::from_complete_history( capture.session_id.clone(), capture.segment_id.clone(), capture.history.clone(), @@ -815,7 +815,7 @@ fn resolve_entry_refs( capture: &crate::feature::session::CommittedSessionCapture, entry_refs: &[String], ) -> Result, ToolError> { - let view = SessionCapture::from_history_entries( + let view = SessionCapture::from_complete_history( capture.session_id.clone(), capture.segment_id.clone(), capture.history.clone(), diff --git a/crates/worker/src/session_capture.rs b/crates/worker/src/session_capture.rs index 979c8d44a..e24d8c1e0 100644 --- a/crates/worker/src/session_capture.rs +++ b/crates/worker/src/session_capture.rs @@ -30,7 +30,7 @@ mod tests { }, ), ]; - let capture = SessionCapture::from_history_entries("session", "segment", entries); + let capture = SessionCapture::from_complete_history("session", "segment", entries); let overview = capture.overview(); assert_eq!(overview.len(), 1); assert!(matches!( @@ -54,8 +54,9 @@ mod tests { }, ); let expected_ref = SessionEntryRef::from_history_entry_id(&retained.annotation.entry_id); - let before = SessionCapture::from_history_entries("session", "old", vec![retained.clone()]); - let after = SessionCapture::from_history_entries("session", "new", vec![retained]); + let before = + SessionCapture::from_complete_history("session", "old", vec![retained.clone()]); + let after = SessionCapture::from_complete_history("session", "new", vec![retained]); assert_eq!(before.overview()[0].id, expected_ref); assert_eq!(after.overview()[0].id, expected_ref); assert_eq!( @@ -96,7 +97,7 @@ mod tests { .iter() .map(|entry| SessionEntryRef::from_history_entry_id(&entry.annotation.entry_id)) .collect::>(); - let capture = SessionCapture::from_history_entries("session", "segment", entries); + let capture = SessionCapture::from_complete_history("session", "segment", entries); assert!(capture.overview().is_empty()); assert!( capture @@ -235,7 +236,7 @@ mod tests { let snapshot: SessionSnapshot = serde_json::from_value(serde_json::to_value(snapshot).unwrap()).unwrap(); for capture in [ - SessionCapture::from_history_entries( + SessionCapture::from_complete_history( session.to_string(), segment.to_string(), restored, @@ -369,7 +370,7 @@ mod tests { assert_eq!(entry.item, items[index]); assert_eq!(entry.annotation, history[index].metadata); } - let capture = SessionCapture::from_history_entries( + let capture = SessionCapture::from_complete_history( session.to_string(), fork.to_string(), restored, @@ -757,7 +758,7 @@ mod tests { let restored = crate::session_history::restore_history_entries(session, first, &log).unwrap(); let fixed = - SessionCapture::from_history_entries(session.to_string(), first.to_string(), restored); + SessionCapture::from_complete_history(session.to_string(), first.to_string(), restored); let overview = fixed.overview()[0].clone(); let evidence = fixed.evidence_for(overview.id.as_str()).unwrap(); let extract = render_extract_input(&fixed); diff --git a/crates/worker/src/worker.rs b/crates/worker/src/worker.rs index 859784866..d47dc40c9 100644 --- a/crates/worker/src/worker.rs +++ b/crates/worker/src/worker.rs @@ -7338,6 +7338,20 @@ impl Worker { .cloned() .collect::>(); project_compactor_capture_attachments(&mut entries_to_summarise); + // Project before Host selection so omitted summary/reference inputs never + // rebase the original source positions of the retained prefix entries. + let mut exploration_entries = history_entries[..retain_from].to_vec(); + project_compactor_capture_attachments(&mut exploration_entries); + let summarised_ids = entries_to_summarise + .iter() + .map(|entry| entry.annotation.entry_id.0.as_str()) + .collect::>(); + let exploration_view = crate::session_capture::SessionCapture::from_complete_history( + self.session_id().to_string(), + self.segment_id().to_string(), + exploration_entries, + ) + .retain_entries(|entry| summarised_ids.contains(entry.entry_id.as_str())); let items_to_summarise = entries_to_summarise .iter() .map(|entry| entry.item.clone()) @@ -7500,11 +7514,7 @@ impl Worker { .with_module( crate::feature::builtin::session_explore::SessionExploreFeature::new( crate::feature::builtin::session_explore::SessionExploreState::new( - crate::session_capture::SessionCapture::from_history_entries( - self.session_id().to_string(), - self.segment_id().to_string(), - entries_to_summarise.clone(), - ), + exploration_view, ), ), ) @@ -17375,7 +17385,7 @@ permission = "write" producer: "arbitrary-context-provider".into() } ); - let view = crate::session_capture::SessionCapture::from_history_entries( + let view = crate::session_capture::SessionCapture::from_complete_history( "session", "startup", replay, ); let extract =