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,21 +358,82 @@ 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 {
|
||||
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,
|
||||
@@ -382,18 +443,35 @@ fn bounded_editor_input(
|
||||
),
|
||||
));
|
||||
}
|
||||
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 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();
|
||||
|
||||
@@ -14,7 +14,7 @@ Selection is deterministic:
|
||||
4. records are allocated round-robin by category, up to 24 records total;
|
||||
5. the 10,000-token material budget is initially divided equally across categories as a fairness reservation; unused shares are then reclaimed in the same canonical round-robin order, so one large category cannot exclude another non-empty category or strand usable capacity;
|
||||
6. both category and aggregate material bounds use the provider-independent estimate `ceil(UTF-8 bytes / 4)`;
|
||||
7. before invoking the editor, the Worker measures the actual normalized Agen request fields containing the rendered system prompt, conversation items (including the fixed question and material JSON), and the typed `SubmitMemorySurface` tool definition/schema against the 12,000-token complete-input budget. It deterministically removes tail materials until the initial request fits, fails rather than publishing a false empty surface if none fit, and guards every subsequent correction request before it reaches the provider. Provider-specific HTTP envelope bytes are outside this normalized request budget.
|
||||
7. before invoking the editor, the Worker measures the actual normalized Agen request fields containing the rendered system prompt, conversation items (including the fixed question and material JSON), and the typed `SubmitMemorySurface` tool definition/schema against the 12,000-token complete-input budget. It reapplies equal per-category fairness reservations under that final estimator, reclaims unused shares in canonical order, fails rather than publishing a false empty surface if no grounded material fits, and guards every subsequent correction request before it reaches the provider. Provider-specific HTTP envelope bytes are outside this normalized request budget.
|
||||
|
||||
The clean-context editor uses the effective `builtin:subjektiv-memory-consolidation` profile model (`codex-oauth/gpt-5.6-luna`, medium reasoning). A generation permits three model turns (the initial attempt plus at most two correction turns). A store-generation conflict causes at most one complete re-read and regeneration. The published Markdown is capped at 1,024 tokens by the same estimate.
|
||||
|
||||
@@ -22,7 +22,7 @@ Every editor point carries one or more exact `{memory_id, revision}` references.
|
||||
|
||||
Publication is idempotent for identical output at one store revision. A different concurrent output cannot replace the first published surface. Confirmed Memory writes atomically mark the prior surface stale. Failed surface work never rolls back confirmed Memory or candidate disposition.
|
||||
|
||||
Resident injection loads only a `ready` snapshot built from the subject's exact current store revision. `un-generated`, `failed`, and `stale` states are omitted rather than represented as "no Memory". A successful empty generation is represented by a current ready snapshot with empty Markdown. Snapshots created before this generation policy existed are retained as immutable history but migrate as `stale` and must be regenerated before injection. Worker prompt materialization remains a start/restore boundary and does not rewrite recorded history.
|
||||
Resident injection loads only a `ready` snapshot built from the subject's exact current store revision. `un-generated`, `failed`, and `stale` states are omitted rather than represented as "no Memory". A successful empty generation is represented by a current ready snapshot with empty Markdown. Snapshots created before this generation policy existed are retained as immutable history but migrate as `stale` and must be regenerated before injection. New Workers materialize the current surface in their initial prompt. Restored Workers preserve that persisted prompt and all prior history verbatim, then append one durable restore-boundary system item that supersedes earlier resident summaries with the latest ready surface or an explicit no-current-surface tombstone; subsequent turns in the same restored process do not append it again.
|
||||
|
||||
## Semantic fixture and verification
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ internalInterruptSystemNote = import "./internal/interrupt_system_note.md";
|
||||
internalWorkingBoundariesSection = import "./internal/working_boundaries_section.md";
|
||||
internalAgentsMdSection = import "./internal/agents_md_section.md";
|
||||
internalResidentMemorySummarySection = import "./internal/resident_memory_summary_section.md";
|
||||
internalResidentMemoryRestoreSection = import "./internal/resident_memory_restore_section.md";
|
||||
internalSubWorkerSpawnToolDescription = import "./internal/sub_worker_spawn_tool_description.md";
|
||||
panelOrchestratorIdleQueueNotice = import "./panel/orchestrator_idle_queue_notice.md";
|
||||
in
|
||||
@@ -72,6 +73,7 @@ in
|
||||
working_boundaries_section = internalWorkingBoundariesSection.content;
|
||||
agents_md_section = internalAgentsMdSection.content;
|
||||
resident_memory_summary_section = internalResidentMemorySummarySection.content;
|
||||
resident_memory_restore_section = internalResidentMemoryRestoreSection.content;
|
||||
worker_orchestration_guidance_section = commonWorkerOrchestration.content;
|
||||
sub_worker_spawn_tool_description = internalSubWorkerSpawnToolDescription.content;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
|
||||
---
|
||||
## Resident memory refresh
|
||||
|
||||
This restore-boundary record supersedes every resident memory summary embedded earlier in the system prompt or session history. Treat only the current state below as resident context; it is background context, not a user request.
|
||||
|
||||
{% if summary %}
|
||||
### Current ready surface
|
||||
|
||||
{{ summary }}
|
||||
{% else %}
|
||||
No current ready resident memory surface is available. Ignore earlier resident memory summaries; confirmed Memory remains available through explicit recall tools.
|
||||
{% endif %}
|
||||
Reference in New Issue
Block a user