fix: preserve role-neutral input policy across compaction and replay

This commit is contained in:
2026-10-20 04:45:50 +09:00
parent 8f759911ab
commit 3b3381900f
46 changed files with 779 additions and 315 deletions
+2 -2
View File
@@ -79,8 +79,8 @@ fn journal_input(
body: body.clone(),
presentation: PublicSystemPresentation::Context,
};
let metadata = SystemInputMetadata::new(
SystemInputUsePolicy {
let metadata = HistoryInputMetadata::new(
HistoryInputUsePolicy {
recording_material: InputEvidenceUse::Exclude,
memory_evidence: InputEvidenceUse::Exclude,
observation: InputObservationUse::Hidden,
+6 -6
View File
@@ -61,12 +61,12 @@ fn resident_input(
schema: &str,
payload: &impl serde::Serialize,
kind: &str,
) -> Result<session_store::LoggedSystemHistoryEntry, session_store::SystemInputError> {
) -> Result<session_store::LoggedSystemHistoryEntry, session_store::InputMetadataError> {
use session_store::*;
let input = SystemInputEnvelope::new(
body.clone(),
SystemInputMetadata::new(
SystemInputUsePolicy {
HistoryInputMetadata::new(
HistoryInputUsePolicy {
recording_material: InputEvidenceUse::Exclude,
memory_evidence: InputEvidenceUse::Exclude,
observation: InputObservationUse::Hidden,
@@ -93,7 +93,7 @@ pub(crate) fn subject_behavior_input(
body: String,
behavior_md: String,
prompt_provenance: Option<session_store::PromptRenderProvenance>,
) -> Result<session_store::LoggedSystemHistoryEntry, session_store::SystemInputError> {
) -> Result<session_store::LoggedSystemHistoryEntry, session_store::InputMetadataError> {
resident_input(
body,
"subject:behavior",
@@ -108,7 +108,7 @@ pub(crate) fn subject_behavior_input(
pub(crate) fn resident_summary_input(
body: String,
prompt_provenance: Option<session_store::PromptRenderProvenance>,
) -> Result<session_store::LoggedSystemHistoryEntry, session_store::SystemInputError> {
) -> Result<session_store::LoggedSystemHistoryEntry, session_store::InputMetadataError> {
resident_input(
body,
"memory:resident-summary",
@@ -891,7 +891,7 @@ impl SystemPromptContributionSource for WorkspaceSubjektivResidentSummarySource
metadata: &crate::session_history::SessionHistoryMetadata,
) {
if let Some(input) = metadata
.system_input
.input_metadata
.as_ref()
.and_then(|input| input.feature_payload())
.and_then(|payload| {
+3 -3
View File
@@ -48,15 +48,15 @@ pub(crate) struct InvocationResultInput {
pub(crate) fn result_input(
result: FeatureInvocationResult,
body: String,
) -> Result<session_store::LoggedSystemHistoryEntry, session_store::SystemInputError> {
) -> Result<session_store::LoggedSystemHistoryEntry, session_store::InputMetadataError> {
use session_store::*;
let payload = FeatureInputPayload::encode(
"feature:invocation-result",
1,
&InvocationResultInput { result },
)?;
let metadata = SystemInputMetadata::new(
SystemInputUsePolicy {
let metadata = HistoryInputMetadata::new(
HistoryInputUsePolicy {
recording_material: InputEvidenceUse::Exclude,
memory_evidence: InputEvidenceUse::Exclude,
observation: InputObservationUse::Hidden,
+1 -1
View File
@@ -1499,7 +1499,7 @@ permission = "write"
is_error: false,
},
metadata: session_store::LoggedSessionHistoryMetadata {
system_input: None,
input_metadata: None,
entry_id: session_store::LoggedSessionHistoryEntryId(entry_id.into()),
origin: session_store::LoggedSessionHistoryOrigin::LegacyUnknown,
derivation: None,
+1 -1
View File
@@ -1033,7 +1033,7 @@ mod tests {
metadata: &crate::session_history::SessionHistoryMetadata,
) {
if let Some(input) = metadata
.system_input
.input_metadata
.as_ref()
.and_then(|input| input.feature_payload())
.and_then(|payload| {
+165 -11
View File
@@ -352,11 +352,19 @@ impl SessionCapture {
let entries = entries
.into_iter()
.map(|entry| {
let (item, use_policy) = if let Some(input) = &entry.annotation.system_input {
(
Item::system_message(input.public_projection().body),
Some(input.public_use_policy()),
)
let (item, use_policy) = if let Some(input) = &entry.annotation.input_metadata {
let item = if matches!(
entry.item,
Item::Message {
role: Role::System,
..
}
) {
Item::system_message(input.public_projection().body)
} else {
entry.item
};
(item, Some(input.public_use_policy()))
} else {
(entry.item, None)
};
@@ -982,10 +990,10 @@ mod tests {
#[test]
fn generic_public_input_observation_and_memory_policies_survive_store_restore_independently() {
use session_store::{
FeatureInputPayload, FsStore, InputCompactionUse, InputEvidenceUse,
InputObservationUse, LogEntry, LoggedSystemHistoryEntry, PublicSystemItem,
PublicSystemPresentation, Store, SystemInputEnvelope, SystemInputMetadata,
SystemInputUsePolicy,
FeatureInputPayload, FsStore, HistoryInputMetadata, HistoryInputUsePolicy,
InputCompactionUse, InputEvidenceUse, InputObservationUse, LogEntry,
LoggedSystemHistoryEntry, PublicSystemItem, PublicSystemPresentation, Store,
SystemInputEnvelope,
};
let root = tempfile::tempdir().unwrap();
let store = FsStore::new(root.path()).unwrap();
@@ -1046,8 +1054,8 @@ mod tests {
None,
SystemInputEnvelope::new(
"private model text",
SystemInputMetadata::new(
SystemInputUsePolicy {
HistoryInputMetadata::new(
HistoryInputUsePolicy {
recording_material: InputEvidenceUse::Exclude,
memory_evidence,
observation,
@@ -1121,6 +1129,152 @@ mod tests {
}
}
// Real FsStore seed/fork/reopen -> ordinary history replay, public snapshot,
// capture and both evidence consumers; no Feature or Tool execution occurs.
#[test]
fn derived_tool_context_keeps_roles_identity_and_policy_through_fork_without_becoming_evidence()
{
use session_store::public_material::{
PublicMaterialLimits, PublicMaterialUse, SessionPublicMaterialReader,
};
use session_store::*;
let root = tempfile::tempdir().unwrap();
let store = FsStore::new(root.path()).unwrap();
let session = new_session_id();
let first = new_segment_id();
let fork = new_segment_id();
let items = vec![
Item::tool_call("original", "ArbitraryTool", "{}"),
Item::tool_result("original", "original result"),
Item::tool_call("derived", "ArbitraryTool", "PRIVATE_SYNTHETIC_CONTEXT"),
Item::tool_result("derived", "PRIVATE_SYNTHETIC_RESULT"),
];
let mut history: Vec<_> = items
.iter()
.cloned()
.map(|item| LoggedHistoryEntry {
item: LoggedItem::from(item),
metadata: LoggedSessionHistoryMetadata::legacy_unknown(),
})
.collect();
let sources = history[..2]
.iter()
.map(|entry| entry.metadata.entry_id.clone())
.collect::<Vec<_>>();
for entry in &mut history[2..] {
entry.metadata.derivation = Some(LoggedHistoryDerivation {
sources: sources.clone(),
});
entry.metadata.input_metadata = Some(
HistoryInputMetadata::new(
HistoryInputUsePolicy {
recording_material: InputEvidenceUse::Exclude,
memory_evidence: InputEvidenceUse::Exclude,
observation: InputObservationUse::Hidden,
compaction: InputCompactionUse::Retain,
},
None,
None,
)
.unwrap(),
);
}
for segment in [first, fork] {
store
.create_segment(
session,
segment,
&[LogEntry::AnnotatedSegmentStart {
ts: 1,
session_id: session,
system_prompt: None,
config: Default::default(),
history: history.clone(),
forked_from: (segment == fork).then_some(SegmentOrigin {
segment_id: first,
at_turn_index: 0,
}),
compacted_from: None,
}],
)
.unwrap();
}
let reopened = FsStore::new(root.path()).unwrap();
let saved = reopened.read_all(session, fork).unwrap();
let restored =
crate::session_history::restore_history_entries(session, fork, &saved).unwrap();
for (index, entry) in restored.iter().enumerate() {
assert_eq!(entry.item, items[index]);
assert_eq!(entry.annotation, history[index].metadata);
}
let capture = SessionCapture::from_history_entries(fork.to_string(), restored);
assert!(
matches!(&capture.entries[2].item, Item::ToolCall { call_id, .. } if call_id == "derived")
);
assert!(
matches!(&capture.entries[3].item, Item::ToolResult { call_id, .. } if call_id == "derived")
);
let snapshot = public_snapshot::project_session_snapshot(session, &saved);
assert!(
!serde_json::to_string(&snapshot)
.unwrap()
.contains("PRIVATE_SYNTHETIC")
);
for capture in [
capture,
SessionCapture::from_session_snapshot(fork.to_string(), snapshot),
] {
assert_eq!(
capture
.search(&SearchOptions {
kind: Some(ReferenceKind::Tool),
..Default::default()
})
.len(),
2
);
for (index, entry) in history.iter().enumerate() {
let reference = SessionEntryRef::from_history_entry_id(&entry.metadata.entry_id);
assert_eq!(
!capture
.read(ReadSelector::Id(reference.as_str()), ReadOptions::default())
.entries
.is_empty(),
index < 2
);
assert_eq!(
capture.evidence_for(reference.as_str()).is_some(),
index < 2
);
}
}
for purpose in [
PublicMaterialUse::Recording,
PublicMaterialUse::MemoryEvidence,
] {
let reader = SessionPublicMaterialReader::open_fs_for_use(
root.path(),
session,
fork,
PublicMaterialLimits::default(),
reopened.retention(),
purpose,
)
.unwrap();
let ids = reader
.descriptors()
.map(|entry| entry.identity.entry_ref.clone())
.collect::<Vec<_>>();
assert_eq!(
ids,
sources
.iter()
.map(|id| format!("E{}", id.0))
.collect::<Vec<_>>()
);
}
}
#[test]
fn overview_contains_user_and_assistant_only() {
let view = SessionCapture::new(
+5 -5
View File
@@ -24,7 +24,7 @@ pub(crate) fn test_logged_history_entry(item: impl Into<Item>) -> LoggedHistoryE
LoggedHistoryEntry {
item: session_store::LoggedItem::from(item.into()),
metadata: LoggedSessionHistoryMetadata {
system_input: None,
input_metadata: None,
entry_id: LoggedSessionHistoryEntryId::new(),
origin: LoggedSessionHistoryOrigin::LegacyUnknown,
derivation: None,
@@ -39,7 +39,7 @@ pub(crate) fn test_logged_system_entry(
session_store::LoggedSystemHistoryEntry {
item,
metadata: LoggedSessionHistoryMetadata {
system_input: None,
input_metadata: None,
entry_id: LoggedSessionHistoryEntryId::new(),
origin: LoggedSessionHistoryOrigin::LegacyUnknown,
derivation: None,
@@ -60,7 +60,7 @@ pub(crate) fn metadata(
derivation: Option<SessionHistoryDerivation>,
) -> SessionHistoryMetadata {
SessionHistoryMetadata {
system_input: None,
input_metadata: None,
entry_id: SessionHistoryEntryId::new(),
origin,
derivation,
@@ -71,7 +71,7 @@ pub(crate) fn compaction_use(
metadata: &SessionHistoryMetadata,
) -> session_store::InputCompactionUse {
metadata
.system_input
.input_metadata
.as_ref()
.map(|input| input.usage().compaction)
.unwrap_or(session_store::InputCompactionUse::Retain)
@@ -93,7 +93,7 @@ pub(crate) fn history_entry_with_id(
HistoryEntry::new(
item,
SessionHistoryMetadata {
system_input: None,
input_metadata: None,
entry_id,
origin,
derivation: None,
+3 -3
View File
@@ -16,7 +16,7 @@ pub(crate) struct SkillActivationInput {
pub(crate) fn activation_input(
activation: &SkillActivationResponse,
) -> Result<session_store::LoggedSystemHistoryEntry, session_store::SystemInputError> {
) -> Result<session_store::LoggedSystemHistoryEntry, session_store::InputMetadataError> {
use session_store::*;
let body = format!(
"Agent Skill `{}` activated from {}.\n\n{}",
@@ -29,8 +29,8 @@ pub(crate) fn activation_input(
name: activation.name.clone(),
},
)?;
let metadata = SystemInputMetadata::new(
SystemInputUsePolicy {
let metadata = HistoryInputMetadata::new(
HistoryInputUsePolicy {
recording_material: InputEvidenceUse::Exclude,
memory_evidence: InputEvidenceUse::Exclude,
observation: InputObservationUse::Hidden,
+1 -1
View File
@@ -1338,7 +1338,7 @@ mod tests {
is_error: false,
},
metadata: session_store::LoggedSessionHistoryMetadata {
system_input: None,
input_metadata: None,
entry_id: session_store::LoggedSessionHistoryEntryId(entry_id.into()),
origin: session_store::LoggedSessionHistoryOrigin::LegacyUnknown,
derivation: None,
+3 -3
View File
@@ -1156,7 +1156,7 @@ enabled = true
LogEntry::AnnotatedSystemItem { entry, .. }
if entry
.metadata
.system_input
.input_metadata
.as_ref()
.and_then(|input| input.feature_payload())
.is_some_and(|payload| payload.schema() == "journal:startup") =>
@@ -1178,7 +1178,7 @@ enabled = true
};
let payload = journal
.metadata
.system_input
.input_metadata
.as_ref()
.unwrap()
.feature_payload()
@@ -1277,7 +1277,7 @@ enabled = true
}
assert_eq!(host.startup_calls.load(Ordering::SeqCst), 1);
let logs = store.read_all(session, segment).unwrap();
assert_eq!(logs.iter().filter(|entry| matches!(entry, LogEntry::AnnotatedSystemItem { entry, .. } if entry.metadata.system_input.as_ref().and_then(|input| input.feature_payload()).is_some_and(|payload| payload.schema() == "journal:startup"))).count(), usize::from(contributes_context));
assert_eq!(logs.iter().filter(|entry| matches!(entry, LogEntry::AnnotatedSystemItem { entry, .. } if entry.metadata.input_metadata.as_ref().and_then(|input| input.feature_payload()).is_some_and(|payload| payload.schema() == "journal:startup"))).count(), usize::from(contributes_context));
controller
.handle
.send(Method::Shutdown {
+1 -1
View File
@@ -5502,7 +5502,7 @@ mod tests {
entry: session_store::LoggedHistoryEntry {
item: session_store::LoggedItem::from(saved),
metadata: session_store::LoggedSessionHistoryMetadata {
system_input: None,
input_metadata: None,
entry_id: session_store::LoggedSessionHistoryEntryId::new(),
origin: session_store::LoggedSessionHistoryOrigin::LegacyUnknown,
derivation: None,
+31 -20
View File
@@ -2739,7 +2739,7 @@ pub trait SystemItemCommitter: Send + Sync {
extensions: Vec<SessionExtension>,
after_commit: &mut dyn FnMut(&HistoryEntry<SessionHistoryMetadata>),
) -> Result<HistoryEntry<SessionHistoryMetadata>, StoreError> {
entry.validate_system_input()?;
entry.validate_input_metadata()?;
let history = HistoryEntry::new(entry.item.to_history_item(), entry.metadata.clone());
self.commit_usage_boundary(
LogEntry::AnnotatedSystemItem {
@@ -6950,7 +6950,7 @@ impl<C: LlmClient + 'static, St: Store> Worker<C, St> {
};
for entry in &frozen_context {
entry
.validate_system_input()
.validate_input_metadata()
.map_err(|e| WorkerError::InvalidState(e.to_string()))?;
}
let history_entries = self.session.history().entries();
@@ -7469,8 +7469,8 @@ impl<C: LlmClient + 'static, St: Store> Worker<C, St> {
new_history[0]
.as_text()
.expect("compaction summary is text"),
session_store::SystemInputMetadata::new(
session_store::SystemInputUsePolicy {
session_store::HistoryInputMetadata::new(
session_store::HistoryInputUsePolicy {
recording_material: session_store::InputEvidenceUse::Exclude,
memory_evidence: session_store::InputEvidenceUse::Exclude,
observation: session_store::InputObservationUse::Hidden,
@@ -7509,15 +7509,26 @@ impl<C: LlmClient + 'static, St: Store> Worker<C, St> {
original_cursor += offset + 1;
HistoryEntry::new(item, original.annotation.clone())
} else {
HistoryEntry::new(
item,
new_history_metadata(
WorkerHistoryProvenance::DerivedSummary,
Some(SessionHistoryDerivation {
sources: derived_sources.clone(),
}),
),
)
let mut metadata = new_history_metadata(
WorkerHistoryProvenance::DerivedSummary,
Some(SessionHistoryDerivation {
sources: derived_sources.clone(),
}),
);
metadata.input_metadata = Some(
session_store::HistoryInputMetadata::new(
session_store::HistoryInputUsePolicy {
recording_material: session_store::InputEvidenceUse::Exclude,
memory_evidence: session_store::InputEvidenceUse::Exclude,
observation: session_store::InputObservationUse::Hidden,
compaction: session_store::InputCompactionUse::Retain,
},
None,
None,
)
.expect("bounded private derived context policy"),
);
HistoryEntry::new(item, metadata)
}
})
.collect::<Vec<_>>();
@@ -10790,9 +10801,9 @@ mod build_summary_prompt_tests {
producer: "arbitrary-summary-source".into(),
},
);
entry.annotation.system_input = Some(
session_store::SystemInputMetadata::new(
session_store::SystemInputUsePolicy {
entry.annotation.input_metadata = Some(
session_store::HistoryInputMetadata::new(
session_store::HistoryInputUsePolicy {
recording_material: session_store::InputEvidenceUse::Exclude,
memory_evidence: session_store::InputEvidenceUse::Exclude,
observation: session_store::InputObservationUse::Hidden,
@@ -14631,7 +14642,7 @@ permission = "write"
matches!(
entry,
LogEntry::AnnotatedSystemItem { entry, .. }
if entry.metadata.system_input.as_ref().and_then(|input| input.feature_payload()).and_then(|payload| payload.decode::<crate::skill::SkillActivationInput>("skill:activation", 1).ok()).is_some_and(|payload| payload.name == "triage-errors")
if entry.metadata.input_metadata.as_ref().and_then(|input| input.feature_payload()).and_then(|payload| payload.decode::<crate::skill::SkillActivationInput>("skill:activation", 1).ok()).is_some_and(|payload| payload.name == "triage-errors")
&& entry.item.history_text().contains("# Triage Errors")
&& entry.item.history_text() == history_text
)
@@ -16919,7 +16930,7 @@ permission = "write"
fn input_schema(entry: &session_store::LoggedSystemHistoryEntry) -> Option<&str> {
entry
.metadata
.system_input
.input_metadata
.as_ref()?
.feature_payload()
.map(|payload| payload.schema())
@@ -16953,8 +16964,8 @@ permission = "write"
None,
SystemInputEnvelope::new(
"PRIOR_JOURNAL_REFERENCE",
SystemInputMetadata::new(
SystemInputUsePolicy {
HistoryInputMetadata::new(
HistoryInputUsePolicy {
recording_material: InputEvidenceUse::Exclude,
memory_evidence: InputEvidenceUse::Exclude,
observation: InputObservationUse::Hidden,
+7 -7
View File
@@ -119,7 +119,7 @@ fn annotated(item: Item) -> session_store::LoggedHistoryEntry {
session_store::LoggedHistoryEntry {
item: session_store::LoggedItem::from(item),
metadata: session_store::LoggedSessionHistoryMetadata {
system_input: None,
input_metadata: None,
entry_id: session_store::LoggedSessionHistoryEntryId::new(),
origin: session_store::LoggedSessionHistoryOrigin::LegacyUnknown,
derivation: None,
@@ -2220,7 +2220,7 @@ fn summary_entry(
origin: session_store::LoggedSessionHistoryOrigin,
) -> session_store::LoggedHistoryEntry {
let mut entry = annotated(item);
entry.metadata.system_input = matches!(
entry.metadata.input_metadata = matches!(
&entry.item,
session_store::LoggedItem::Message {
role: session_store::LoggedRole::System,
@@ -2228,8 +2228,8 @@ fn summary_entry(
}
)
.then(|| {
session_store::SystemInputMetadata::new(
session_store::SystemInputUsePolicy {
session_store::HistoryInputMetadata::new(
session_store::HistoryInputUsePolicy {
recording_material: session_store::InputEvidenceUse::Exclude,
memory_evidence: session_store::InputEvidenceUse::Exclude,
observation: session_store::InputObservationUse::Hidden,
@@ -2256,7 +2256,7 @@ fn compaction_use(
metadata: &session_store::LoggedSessionHistoryMetadata,
) -> session_store::InputCompactionUse {
metadata
.system_input
.input_metadata
.as_ref()
.map(|input| input.usage().compaction)
.unwrap_or(session_store::InputCompactionUse::Retain)
@@ -3167,8 +3167,8 @@ async fn journal_three_compactions_freeze_one_snapshot_preserve_full_summary_tai
None,
session_store::SystemInputEnvelope::new(
"OLD_TYPED_STARTUP_JOURNAL",
session_store::SystemInputMetadata::new(
session_store::SystemInputUsePolicy {
session_store::HistoryInputMetadata::new(
session_store::HistoryInputUsePolicy {
recording_material: session_store::InputEvidenceUse::Exclude,
memory_evidence: session_store::InputEvidenceUse::Exclude,
observation: session_store::InputObservationUse::Hidden,
@@ -231,7 +231,7 @@ fn results(worker: &Worker<Trace, TestStore>) -> Vec<FeatureInvocationResult> {
}
entry
.metadata
.system_input
.input_metadata
.as_ref()?
.feature_payload()?
.decode::<SavedResult>("feature:invocation-result", 1)