fix: refresh memory surfaces on restore
This commit is contained in:
@@ -157,6 +157,16 @@ pub enum SystemItem {
|
||||
prompt_provenance: Option<PromptRenderProvenance>,
|
||||
},
|
||||
|
||||
/// Restore-boundary refresh of the current resident Memory projection.
|
||||
/// The exact rendered body is retained so replay is byte-identical; a
|
||||
/// refresh with no ready summary explicitly supersedes older resident
|
||||
/// context without deleting or rewriting history.
|
||||
ResidentSummaryRefresh {
|
||||
body: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
prompt_provenance: Option<PromptRenderProvenance>,
|
||||
},
|
||||
|
||||
/// Synthetic note inserted after an interrupted turn before the next
|
||||
/// user input. `body` is the exact LLM-context text explaining that the
|
||||
/// previous turn was cut short.
|
||||
@@ -181,6 +191,7 @@ impl SystemItem {
|
||||
format!("Ignored legacy procedure item: /{slug}")
|
||||
}
|
||||
SystemItem::TaskReminder { body, .. } => body.clone(),
|
||||
SystemItem::ResidentSummaryRefresh { body, .. } => body.clone(),
|
||||
SystemItem::Interrupt { body, .. } => body.clone(),
|
||||
}
|
||||
}
|
||||
@@ -202,6 +213,7 @@ impl SystemItem {
|
||||
SystemItem::LegacyKnowledgeIgnored { .. } => "legacy_knowledge_ignored",
|
||||
SystemItem::LegacyIgnored { .. } => "legacy_ignored",
|
||||
SystemItem::TaskReminder { .. } => "task_reminder",
|
||||
SystemItem::ResidentSummaryRefresh { .. } => "resident_summary_refresh",
|
||||
SystemItem::Interrupt { .. } => "interrupt",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2584,6 +2584,7 @@ impl App {
|
||||
}
|
||||
session_store::SystemItem::FileAttachment { body, .. }
|
||||
| session_store::SystemItem::SkillActivation { body, .. }
|
||||
| session_store::SystemItem::ResidentSummaryRefresh { body, .. }
|
||||
| session_store::SystemItem::Interrupt { body, .. } => {
|
||||
self.task_store.apply_system_message_text(&body);
|
||||
self.blocks.push(Block::SystemMessage { text: body });
|
||||
|
||||
@@ -358,42 +358,120 @@ fn bounded_editor_input(
|
||||
system_prompt: &str,
|
||||
generation: &server_api::SubjektivSurfacePrepareResponse,
|
||||
) -> Result<(String, Vec<server_api::SubjektivSurfaceMaterial>), HookError> {
|
||||
let mut materials = generation.materials.clone();
|
||||
loop {
|
||||
let input = serde_json::to_string(&serde_json::json!({
|
||||
"question": EDITOR_QUESTION,
|
||||
"body_token_budget": generation.body_token_budget,
|
||||
"input_token_budget": generation.input_token_budget,
|
||||
"materials": &materials,
|
||||
}))
|
||||
.map_err(|error| HookError::new(HookErrorCategory::Internal, error.to_string()))?;
|
||||
let request = Request::new()
|
||||
.system(system_prompt)
|
||||
.user(input.clone())
|
||||
.tool(submit_llm_tool_definition());
|
||||
let estimated_tokens = estimated_editor_request_tokens(&request)?;
|
||||
if estimated_tokens <= generation.input_token_budget {
|
||||
if materials.is_empty() && !generation.materials.is_empty() {
|
||||
return Err(HookError::new(
|
||||
HookErrorCategory::Internal,
|
||||
format!(
|
||||
"surface editor fixed prompt and material overhead left no grounded material within the {} token input budget",
|
||||
generation.input_token_budget
|
||||
),
|
||||
));
|
||||
}
|
||||
return Ok((input, materials));
|
||||
}
|
||||
if materials.pop().is_none() {
|
||||
return Err(HookError::new(
|
||||
HookErrorCategory::Internal,
|
||||
format!(
|
||||
"surface editor fixed prompt, JSON framing, and tool schema exceed the {} token input budget (estimated {estimated_tokens})",
|
||||
generation.input_token_budget
|
||||
),
|
||||
));
|
||||
let empty_tokens = editor_request_tokens_for_materials(system_prompt, generation, &[])?;
|
||||
if empty_tokens > generation.input_token_budget {
|
||||
return Err(HookError::new(
|
||||
HookErrorCategory::Internal,
|
||||
format!(
|
||||
"surface editor fixed prompt, JSON framing, and tool schema exceed the {} token input budget (estimated {empty_tokens})",
|
||||
generation.input_token_budget
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
let mut populated_kinds = Vec::new();
|
||||
for material in &generation.materials {
|
||||
if !populated_kinds.contains(&material.kind) {
|
||||
populated_kinds.push(material.kind.clone());
|
||||
}
|
||||
}
|
||||
let material_token_budget = generation.input_token_budget - empty_tokens;
|
||||
let per_kind_token_budget = material_token_budget / populated_kinds.len().max(1);
|
||||
let mut accepted = vec![false; generation.materials.len()];
|
||||
let mut deferred = Vec::new();
|
||||
|
||||
for (index, material) in generation.materials.iter().enumerate() {
|
||||
let kind_materials = generation
|
||||
.materials
|
||||
.iter()
|
||||
.zip(&accepted)
|
||||
.filter(|(selected, accepted)| **accepted && selected.kind == material.kind)
|
||||
.map(|(selected, _)| selected)
|
||||
.chain(std::iter::once(material))
|
||||
.collect::<Vec<_>>();
|
||||
let all_materials = generation
|
||||
.materials
|
||||
.iter()
|
||||
.zip(&accepted)
|
||||
.filter(|(_, accepted)| **accepted)
|
||||
.map(|(selected, _)| selected)
|
||||
.chain(std::iter::once(material))
|
||||
.collect::<Vec<_>>();
|
||||
let kind_cost =
|
||||
editor_request_tokens_for_materials(system_prompt, generation, &kind_materials)?
|
||||
.saturating_sub(empty_tokens);
|
||||
let all_tokens =
|
||||
editor_request_tokens_for_materials(system_prompt, generation, &all_materials)?;
|
||||
if kind_cost <= per_kind_token_budget && all_tokens <= generation.input_token_budget {
|
||||
accepted[index] = true;
|
||||
} else {
|
||||
deferred.push(index);
|
||||
}
|
||||
}
|
||||
|
||||
// Equal per-kind shares reserve room in the final normalized request. Any
|
||||
// unused share is reclaimed in the original canonical round-robin order.
|
||||
for index in deferred {
|
||||
let material = &generation.materials[index];
|
||||
let all_materials = generation
|
||||
.materials
|
||||
.iter()
|
||||
.zip(&accepted)
|
||||
.filter(|(_, accepted)| **accepted)
|
||||
.map(|(selected, _)| selected)
|
||||
.chain(std::iter::once(material))
|
||||
.collect::<Vec<_>>();
|
||||
if editor_request_tokens_for_materials(system_prompt, generation, &all_materials)?
|
||||
<= generation.input_token_budget
|
||||
{
|
||||
accepted[index] = true;
|
||||
}
|
||||
}
|
||||
|
||||
let materials = generation
|
||||
.materials
|
||||
.iter()
|
||||
.zip(accepted)
|
||||
.filter_map(|(material, accepted)| accepted.then_some(material.clone()))
|
||||
.collect::<Vec<_>>();
|
||||
if materials.is_empty() && !generation.materials.is_empty() {
|
||||
return Err(HookError::new(
|
||||
HookErrorCategory::Internal,
|
||||
format!(
|
||||
"surface editor fixed prompt and material overhead left no grounded material within the {} token input budget",
|
||||
generation.input_token_budget
|
||||
),
|
||||
));
|
||||
}
|
||||
let material_refs = materials.iter().collect::<Vec<_>>();
|
||||
let input = serialize_editor_input(generation, &material_refs)?;
|
||||
Ok((input, materials))
|
||||
}
|
||||
|
||||
fn editor_request_tokens_for_materials(
|
||||
system_prompt: &str,
|
||||
generation: &server_api::SubjektivSurfacePrepareResponse,
|
||||
materials: &[&server_api::SubjektivSurfaceMaterial],
|
||||
) -> Result<usize, HookError> {
|
||||
let input = serialize_editor_input(generation, materials)?;
|
||||
let request = Request::new()
|
||||
.system(system_prompt)
|
||||
.user(input)
|
||||
.tool(submit_llm_tool_definition());
|
||||
estimated_editor_request_tokens(&request)
|
||||
}
|
||||
|
||||
fn serialize_editor_input(
|
||||
generation: &server_api::SubjektivSurfacePrepareResponse,
|
||||
materials: &[&server_api::SubjektivSurfaceMaterial],
|
||||
) -> Result<String, HookError> {
|
||||
serde_json::to_string(&serde_json::json!({
|
||||
"question": EDITOR_QUESTION,
|
||||
"body_token_budget": generation.body_token_budget,
|
||||
"input_token_budget": generation.input_token_budget,
|
||||
"materials": materials,
|
||||
}))
|
||||
.map_err(|error| HookError::new(HookErrorCategory::Internal, error.to_string()))
|
||||
}
|
||||
|
||||
fn estimated_editor_request_tokens(request: &Request) -> Result<usize, HookError> {
|
||||
@@ -431,11 +509,19 @@ mod tests {
|
||||
use crate::worker::{WorkspaceClientError, WorkspaceRequest, WorkspaceResponse};
|
||||
|
||||
fn material(id: &str, body_bytes: usize) -> server_api::SubjektivSurfaceMaterial {
|
||||
material_with_body(id, CandidateKind::Constraint, "x".repeat(body_bytes))
|
||||
}
|
||||
|
||||
fn material_with_body(
|
||||
id: &str,
|
||||
kind: CandidateKind,
|
||||
body_md: String,
|
||||
) -> server_api::SubjektivSurfaceMaterial {
|
||||
server_api::SubjektivSurfaceMaterial {
|
||||
memory_id: id.into(),
|
||||
revision: 1,
|
||||
kind: CandidateKind::Constraint,
|
||||
body_md: "x".repeat(body_bytes),
|
||||
kind,
|
||||
body_md,
|
||||
why_useful: "budget fixture".into(),
|
||||
staleness: None,
|
||||
}
|
||||
@@ -703,7 +789,7 @@ permission = "write"
|
||||
);
|
||||
let (input, selected) = bounded_editor_input("system policy", &generation).unwrap();
|
||||
|
||||
assert_eq!(selected.len(), 1, "deterministically trims the tail");
|
||||
assert_eq!(selected.len(), 1, "deterministically bounds one category");
|
||||
assert_eq!(selected[0].memory_id, "memory-1");
|
||||
assert!(input.contains(EDITOR_QUESTION));
|
||||
assert!(
|
||||
@@ -722,6 +808,77 @@ permission = "write"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn final_editor_budget_preserves_feasible_nonempty_categories() {
|
||||
let materials = vec![
|
||||
material_with_body(
|
||||
"preference-large",
|
||||
CandidateKind::Preference,
|
||||
"\\\"".repeat(8_000),
|
||||
),
|
||||
material_with_body(
|
||||
"constraint-small",
|
||||
CandidateKind::Constraint,
|
||||
"constraint".into(),
|
||||
),
|
||||
material_with_body("decision-small", CandidateKind::Decision, "decision".into()),
|
||||
material_with_body(
|
||||
"assumption-small",
|
||||
CandidateKind::WorkingAssumption,
|
||||
"assumption".into(),
|
||||
),
|
||||
material_with_body(
|
||||
"question-small",
|
||||
CandidateKind::OpenQuestion,
|
||||
"question".into(),
|
||||
),
|
||||
material_with_body("lesson-small", CandidateKind::Lesson, "lesson".into()),
|
||||
];
|
||||
let mut generation = generation(9_999, materials);
|
||||
for _ in 0..3 {
|
||||
let large = editor_request_tokens_for_materials(
|
||||
"system policy",
|
||||
&generation,
|
||||
&[&generation.materials[0]],
|
||||
)
|
||||
.unwrap();
|
||||
let small = editor_request_tokens_for_materials(
|
||||
"system policy",
|
||||
&generation,
|
||||
&generation.materials[1..].iter().collect::<Vec<_>>(),
|
||||
)
|
||||
.unwrap();
|
||||
generation.input_token_budget = large.max(small);
|
||||
}
|
||||
let all = editor_request_tokens_for_materials(
|
||||
"system policy",
|
||||
&generation,
|
||||
&generation.materials.iter().collect::<Vec<_>>(),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(all > generation.input_token_budget);
|
||||
|
||||
let (input, selected) = bounded_editor_input("system policy", &generation).unwrap();
|
||||
assert!(
|
||||
!selected
|
||||
.iter()
|
||||
.any(|item| item.kind == CandidateKind::Preference)
|
||||
);
|
||||
for kind in [
|
||||
CandidateKind::Constraint,
|
||||
CandidateKind::Decision,
|
||||
CandidateKind::WorkingAssumption,
|
||||
CandidateKind::OpenQuestion,
|
||||
CandidateKind::Lesson,
|
||||
] {
|
||||
assert!(selected.iter().any(|item| item.kind == kind));
|
||||
}
|
||||
assert!(
|
||||
estimated_editor_request_tokens(&editor_request("system policy", &input)).unwrap()
|
||||
<= generation.input_token_budget
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn actual_editor_request_is_rejected_before_provider_when_over_budget() {
|
||||
let calls = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
@@ -104,6 +104,7 @@ pub enum WorkerPrompt {
|
||||
WorkingBoundariesSection,
|
||||
AgentsMdSection,
|
||||
ResidentMemorySummarySection,
|
||||
ResidentMemoryRestoreSection,
|
||||
WorkerOrchestrationGuidanceSection,
|
||||
SubWorkerSpawnToolDescription,
|
||||
}
|
||||
@@ -126,6 +127,7 @@ impl WorkerPrompt {
|
||||
Self::WorkingBoundariesSection => "internal.working_boundaries_section",
|
||||
Self::AgentsMdSection => "internal.agents_md_section",
|
||||
Self::ResidentMemorySummarySection => "internal.resident_memory_summary_section",
|
||||
Self::ResidentMemoryRestoreSection => "internal.resident_memory_restore_section",
|
||||
Self::WorkerOrchestrationGuidanceSection => {
|
||||
"internal.worker_orchestration_guidance_section"
|
||||
}
|
||||
@@ -147,6 +149,7 @@ impl WorkerPrompt {
|
||||
WorkerPrompt::WorkingBoundariesSection,
|
||||
WorkerPrompt::AgentsMdSection,
|
||||
WorkerPrompt::ResidentMemorySummarySection,
|
||||
WorkerPrompt::ResidentMemoryRestoreSection,
|
||||
WorkerPrompt::WorkerOrchestrationGuidanceSection,
|
||||
WorkerPrompt::SubWorkerSpawnToolDescription,
|
||||
];
|
||||
@@ -502,6 +505,15 @@ impl PromptCatalog {
|
||||
single("summary", summary),
|
||||
)
|
||||
}
|
||||
pub fn resident_memory_restore_section(
|
||||
&self,
|
||||
summary: Option<&str>,
|
||||
) -> Result<String, CatalogError> {
|
||||
self.render(
|
||||
WorkerPrompt::ResidentMemoryRestoreSection,
|
||||
single("summary", summary.unwrap_or_default()),
|
||||
)
|
||||
}
|
||||
pub fn worker_orchestration_guidance_section(&self) -> Result<String, CatalogError> {
|
||||
self.render(
|
||||
WorkerPrompt::WorkerOrchestrationGuidanceSection,
|
||||
|
||||
+222
-3
@@ -411,7 +411,9 @@ use crate::ipc::alerter::Alerter;
|
||||
use crate::ipc::interceptor::WorkerInterceptor;
|
||||
use crate::ipc::notify_buffer::NotifyBuffer;
|
||||
use crate::prompt::agents_md::read_agents_md;
|
||||
use crate::prompt::catalog::{CatalogError, PromptCatalog, WorkspacePromptProjection};
|
||||
use crate::prompt::catalog::{
|
||||
CatalogError, PromptCatalog, WorkerPrompt, WorkspacePromptProjection,
|
||||
};
|
||||
use crate::prompt::source::PromptCatalogSource;
|
||||
use crate::prompt::system::{SystemPromptContext, SystemPromptError, SystemPromptTemplate};
|
||||
use crate::runtime::dir;
|
||||
@@ -2500,6 +2502,8 @@ pub struct Worker<C: LlmClient, St: Store> {
|
||||
inject_resident_summary: bool,
|
||||
/// Deferred resident prompt source installed by an enabled Feature.
|
||||
feature_resident_summary_source: Option<Arc<dyn SystemPromptContributionSource>>,
|
||||
/// One-shot append-only resident refresh required at a restore boundary.
|
||||
restore_resident_summary_refresh_pending: bool,
|
||||
/// Complete system prompt replacement installed by an enabled Feature.
|
||||
feature_system_prompt_override: Option<String>,
|
||||
/// Typed user submissions in submit order. K-th entry corresponds to
|
||||
@@ -2753,6 +2757,7 @@ impl<C: LlmClient + 'static, St: Store> Worker<C, St> {
|
||||
prompts,
|
||||
inject_resident_summary: true,
|
||||
feature_resident_summary_source: None,
|
||||
restore_resident_summary_refresh_pending: false,
|
||||
feature_system_prompt_override: None,
|
||||
user_segments: Vec::new(),
|
||||
sink: SegmentLogSink::new(),
|
||||
@@ -3819,6 +3824,62 @@ impl<C: LlmClient + 'static, St: Store> Worker<C, St> {
|
||||
}
|
||||
}
|
||||
|
||||
async fn ensure_restore_resident_summary_refreshed(&mut self) -> Result<(), WorkerError>
|
||||
where
|
||||
St: Clone + 'static,
|
||||
{
|
||||
if !self.restore_resident_summary_refresh_pending {
|
||||
return Ok(());
|
||||
}
|
||||
let Some(source) = self.feature_resident_summary_source.clone() else {
|
||||
self.restore_resident_summary_refresh_pending = false;
|
||||
return Ok(());
|
||||
};
|
||||
let summary = if self.inject_resident_summary {
|
||||
source.load().await
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let prompt_catalog = self.prompts.load_full();
|
||||
let body = prompt_catalog.resident_memory_restore_section(summary.as_deref())?;
|
||||
let prompt_provenance =
|
||||
Some(self.prompt_render_provenance(WorkerPrompt::ResidentMemoryRestoreSection.key()));
|
||||
let item = SystemItem::ResidentSummaryRefresh {
|
||||
body: body.clone(),
|
||||
prompt_provenance,
|
||||
};
|
||||
let metadata = new_history_metadata(
|
||||
WorkerHistoryProvenance::BackendInstruction { operation_id: None },
|
||||
None,
|
||||
);
|
||||
self.commit_entry(LogEntry::AnnotatedSystemItem {
|
||||
ts: segment_log::now_millis(),
|
||||
entry: session_store::LoggedSystemHistoryEntry {
|
||||
item,
|
||||
metadata: metadata.clone(),
|
||||
},
|
||||
extensions: Vec::new(),
|
||||
})?;
|
||||
let history_entry = HistoryEntry::new(Item::system_message(body), metadata);
|
||||
let mut annotate = history_annotator(
|
||||
self.log_writer_handle(),
|
||||
vec![history_entry.clone()],
|
||||
self.pending_committed_history.clone(),
|
||||
);
|
||||
let (engine, session) = (
|
||||
self.engine.as_mut().expect("worker present"),
|
||||
&mut self.session,
|
||||
);
|
||||
engine.append_history_with(
|
||||
session.history_mut(),
|
||||
std::iter::once(history_entry.item),
|
||||
&mut annotate,
|
||||
)?;
|
||||
session.note_mutation();
|
||||
self.restore_resident_summary_refresh_pending = false;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Render the manifest-supplied instruction template exactly once,
|
||||
/// just before the first LLM turn, append the fixed trailing
|
||||
/// section (scope summary + optional AGENTS.md), and hand the
|
||||
@@ -3826,9 +3887,12 @@ impl<C: LlmClient + 'static, St: Store> Worker<C, St> {
|
||||
/// Subsequent invocations are no-ops: the template field is
|
||||
/// consumed with `Option::take()`, so the materialised value
|
||||
/// persists across all later turns and compaction.
|
||||
async fn ensure_system_prompt_materialized(&mut self) -> Result<(), WorkerError> {
|
||||
async fn ensure_system_prompt_materialized(&mut self) -> Result<(), WorkerError>
|
||||
where
|
||||
St: Clone + 'static,
|
||||
{
|
||||
let Some(template) = self.system_prompt_template.take() else {
|
||||
return Ok(());
|
||||
return self.ensure_restore_resident_summary_refreshed().await;
|
||||
};
|
||||
if let Some(rendered) = self.feature_system_prompt_override.take() {
|
||||
self.engine
|
||||
@@ -6549,6 +6613,7 @@ where
|
||||
prompts: common.prompts,
|
||||
inject_resident_summary: true,
|
||||
feature_resident_summary_source: None,
|
||||
restore_resident_summary_refresh_pending: false,
|
||||
feature_system_prompt_override: None,
|
||||
user_segments: Vec::new(),
|
||||
sink: SegmentLogSink::new(),
|
||||
@@ -6638,6 +6703,7 @@ where
|
||||
prompts: common.prompts,
|
||||
inject_resident_summary: true,
|
||||
feature_resident_summary_source: None,
|
||||
restore_resident_summary_refresh_pending: false,
|
||||
feature_system_prompt_override: None,
|
||||
user_segments: Vec::new(),
|
||||
sink: SegmentLogSink::new(),
|
||||
@@ -6762,6 +6828,7 @@ where
|
||||
prompts: common.prompts,
|
||||
inject_resident_summary: true,
|
||||
feature_resident_summary_source: None,
|
||||
restore_resident_summary_refresh_pending: false,
|
||||
feature_system_prompt_override: None,
|
||||
user_segments: Vec::new(),
|
||||
sink: SegmentLogSink::new(),
|
||||
@@ -7143,6 +7210,7 @@ where
|
||||
prompts: common.prompts,
|
||||
inject_resident_summary: true,
|
||||
feature_resident_summary_source: None,
|
||||
restore_resident_summary_refresh_pending: true,
|
||||
feature_system_prompt_override: None,
|
||||
user_segments: state.user_segments,
|
||||
// Seed the mirror with the entries we just replayed so a
|
||||
@@ -11075,6 +11143,157 @@ mod build_summary_prompt_tests {
|
||||
assert!(worker.history().is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn restored_worker_appends_latest_resident_surface_without_rewriting_history() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let cwd = dir.path().join("workspace");
|
||||
std::fs::create_dir_all(&cwd).unwrap();
|
||||
let runtime = dir.path().join("runtime");
|
||||
std::fs::create_dir_all(&runtime).unwrap();
|
||||
let _runtime =
|
||||
crate::runtime::worker_allocation::test_util::RuntimeDirSandbox::new(&runtime);
|
||||
let store = session_store::CombinedStore::new(
|
||||
session_store::FsStore::new(dir.path().join("sessions")).unwrap(),
|
||||
session_store::FsWorkerStore::new(dir.path().join("workers")).unwrap(),
|
||||
);
|
||||
let manifest = WorkerManifest::from_toml(&format!(
|
||||
r#"
|
||||
[worker]
|
||||
name = "restore-resident-{}"
|
||||
|
||||
[model]
|
||||
scheme = "anthropic"
|
||||
model_id = "test-model"
|
||||
|
||||
[engine]
|
||||
instruction = "default"
|
||||
|
||||
[[scope.allow]]
|
||||
target = "{}"
|
||||
permission = "write"
|
||||
"#,
|
||||
uuid::Uuid::now_v7(),
|
||||
cwd.display()
|
||||
))
|
||||
.unwrap();
|
||||
let authority = WorkerFilesystemAuthority::local(cwd.clone(), cwd.clone());
|
||||
let scope = Scope::writable(&cwd).unwrap();
|
||||
let mut original = Worker::new(
|
||||
manifest.clone(),
|
||||
Engine::<_, Mutable, SessionHistoryMetadata>::new_annotated(NoopClient),
|
||||
store.clone(),
|
||||
WorkerWorkspaceContext::no_workspace(),
|
||||
authority.clone(),
|
||||
scope,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
original.set_system_prompt_template(
|
||||
SystemPromptTemplate::parse(
|
||||
"default",
|
||||
crate::prompt::source::PromptCatalogSource::builtins_only(),
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
let (old_source, _) =
|
||||
test_system_prompt_contribution_source(Some("old subject surface".into()));
|
||||
original.install_system_prompt_contribution(Some(old_source), None);
|
||||
original.materialize_durable_session_head().await.unwrap();
|
||||
let session_id = original.session_id();
|
||||
let segment_id = original.segment_id();
|
||||
assert!(
|
||||
original
|
||||
.engine()
|
||||
.get_system_prompt()
|
||||
.unwrap()
|
||||
.contains("old subject surface")
|
||||
);
|
||||
drop(original);
|
||||
|
||||
let mut restored =
|
||||
Worker::<Box<dyn LlmClient>, _>::restore_from_manifest_with_context_and_model_client(
|
||||
session_id,
|
||||
segment_id,
|
||||
manifest.clone(),
|
||||
store.clone(),
|
||||
crate::prompt::source::PromptCatalogSource::builtins_only(),
|
||||
WorkerWorkspaceContext::no_workspace(),
|
||||
authority.clone(),
|
||||
Some(Box::new(NoopClient)),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let history_before = restored.history().to_vec();
|
||||
let (current_source, current_loads) =
|
||||
test_system_prompt_contribution_source(Some("latest subject surface".into()));
|
||||
restored.install_system_prompt_contribution(Some(current_source), None);
|
||||
restored.materialize_durable_session_head().await.unwrap();
|
||||
|
||||
assert_eq!(current_loads.load(Ordering::SeqCst), 1);
|
||||
assert_eq!(&restored.history()[..history_before.len()], history_before);
|
||||
let refresh = restored
|
||||
.history()
|
||||
.last()
|
||||
.unwrap()
|
||||
.as_text()
|
||||
.unwrap()
|
||||
.to_string();
|
||||
assert!(refresh.contains("latest subject surface"));
|
||||
assert!(refresh.contains("supersedes every resident memory summary"));
|
||||
assert!(!refresh.contains("old subject surface"));
|
||||
let refreshed_len = restored.history().len();
|
||||
restored.materialize_durable_session_head().await.unwrap();
|
||||
assert_eq!(restored.history().len(), refreshed_len);
|
||||
drop(restored);
|
||||
|
||||
let mut stale_restore =
|
||||
Worker::<Box<dyn LlmClient>, _>::restore_from_manifest_with_context_and_model_client(
|
||||
session_id,
|
||||
segment_id,
|
||||
manifest,
|
||||
store.clone(),
|
||||
crate::prompt::source::PromptCatalogSource::builtins_only(),
|
||||
WorkerWorkspaceContext::no_workspace(),
|
||||
authority,
|
||||
Some(Box::new(NoopClient)),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let stale_history_before = stale_restore.history().to_vec();
|
||||
let (stale_source, stale_loads) = test_system_prompt_contribution_source(None);
|
||||
stale_restore.install_system_prompt_contribution(Some(stale_source), None);
|
||||
stale_restore
|
||||
.materialize_durable_session_head()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(stale_loads.load(Ordering::SeqCst), 1);
|
||||
assert_eq!(
|
||||
&stale_restore.history()[..stale_history_before.len()],
|
||||
stale_history_before
|
||||
);
|
||||
let tombstone = stale_restore
|
||||
.history()
|
||||
.last()
|
||||
.unwrap()
|
||||
.as_text()
|
||||
.unwrap()
|
||||
.to_string();
|
||||
assert!(tombstone.contains("No current ready resident memory surface"));
|
||||
assert!(!tombstone.contains("latest subject surface"));
|
||||
let entries = store.read_all(session_id, segment_id).unwrap();
|
||||
assert!(matches!(
|
||||
entries.last(),
|
||||
Some(LogEntry::AnnotatedSystemItem {
|
||||
entry: session_store::LoggedSystemHistoryEntry {
|
||||
item: SystemItem::ResidentSummaryRefresh { .. },
|
||||
..
|
||||
},
|
||||
..
|
||||
})
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn memory_consolidation_prompt_uses_bound_workspace_language() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
|
||||
Reference in New Issue
Block a user