From e1ff33e1c37db539802bb2ab22a010b1d424d8b1 Mon Sep 17 00:00:00 2001 From: ebigunso Date: Wed, 23 Sep 2026 00:30:42 +0900 Subject: [PATCH 1/3] =?UTF-8?q?=E2=9A=A0=EF=B8=8F=20Warn=20about=20disconn?= =?UTF-8?q?ected=20resolutions=20and=20repeated=20participants?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 14 +- src/domain/write_validation.rs | 11 + src/memory.rs | 478 +++++++++++++++++++++++++++++++++ src/usecases/write_planning.rs | 69 +++++ 4 files changed, 571 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 91170b3e..872e5ba9 100644 --- a/README.md +++ b/README.md @@ -187,11 +187,23 @@ An episode stores one `Scene` with its time, participants, setting and custom va The write path is deliberately not an extraction system. Character Memory core does not infer preferences, commitments, corrections, character signals, thread membership, or entity identity from raw text. It does not store raw logs, and `raw_ref` values remain opaque caller-managed provenance pointers. Candidates in a `RememberWritePlan` are not memory until a valid plan is committed. +### Write warnings + +A valid write can carry warnings. Read each candidate's `warnings` in the validations returned by `validate_plan`, or in `RememberOutcome.diagnostics.validations` after `remember` or `commit`. Each warning has a `kind` tag and identifies the affected memories or participant key; `candidate_index` and `candidate_kind` identify the candidate. The same warnings are also projected into `diagnostics.messages` with warning severity and the code `WritePlanValidationWarning`. Warnings neither refuse a write nor change what is stored. + +- `duplicate_observation_echo`: an observation or interpreted memory repeats its source episode's summary exactly, so that wording is stored and indexed again. +- `resolver_without_shared_subject_or_thread`: a `Resolves` or `FulfillsCommitment` link joins interpreted memories with no shared subject and no shared thread. The matter leaves where things stand, but meeting the person alone will not bring the memory that says it was settled. The warning belongs to the link candidate and names the resolver and target, including when either is already stored. +- `repeated_scene_participant`: the same participant key occurs more than once in an episode's scene. Every entry is kept and reported, and its words contribute again to the participant search text; the write still creates only one participant link per distinct key. There is one warning per repeated key. Identical descriptions without keys do not establish that two participants are the same person. + +`remember(RememberInput::new("Alice asked about the trip"), RememberOptions::default())` uses the same text for the episode summary and observation, so it carries the echo warning. Give the episode a distinct summary with `.with_episode(EpisodeDraft::new("Planning a trip with Alice"))`, or supply a distinct observation, to keep the two texts different. A resolution written through the separate `link` method carries no validation warnings; a link-only `RememberWritePlan` passed to `validate_plan` or `commit` does. + +Recency and last met see an experience only at reference times at or after its scene time. A later scene time is stored as given and does not itself produce a warning. + ## Notions and naming beliefs An `Entity` represents a notion the character holds: an id, object type, creation time and schema version. An entity has no name. Construct its draft with `EntityDraft::new()` and supply or retain its id. Names and other descriptions belong to ordinary `DerivedMemory` beliefs about that id. A belief's `assertions: Vec` can carry `BeliefPredicate::KnownAs { name }` for a subject in its `entity_ids`. An assertion is the character's own commitment; hearsay, doubt and aspects without a mechanical reader remain plain text. -Set `given_by_application = true` when the application gives a belief about a notion without source episodes or observations. That declaration requires a notion subject and cannot coexist with source experiences. With neither sources nor the declaration, admission fails. To initialize a notion and a given belief before any experience, author their candidates in a `RememberWritePlan` with explicit ids, timestamps and schema versions; validate and commit the plan. The `RememberInput` convenience path also creates an episode and observation for its input, but does not attach those as sources to a given belief. +Set `given_by_application = true` when the application gives a belief about a notion without source episodes or observations. That declaration requires a notion subject and cannot coexist with source experiences. Without source experiences, the belief has no setting or custom context to recall it by; its subjects and the topic can still bring it to mind. With neither sources nor the declaration, admission fails. To initialize a notion and a given belief before any experience, author their candidates in a `RememberWritePlan` with explicit ids, timestamps and schema versions; validate and commit the plan. The `RememberInput` convenience path also creates an episode and observation for its input, but does not attach those as sources to a given belief. A rename is an ordinary supersession. With caller-retained `notion_id`, `old_belief_id` and a fresh `new_belief_id`, prepare the new belief, inspect validation and commit: diff --git a/src/domain/write_validation.rs b/src/domain/write_validation.rs index dd641407..1779f228 100644 --- a/src/domain/write_validation.rs +++ b/src/domain/write_validation.rs @@ -124,6 +124,17 @@ pub enum CandidateValidationIssue { echo_surface: String, matching_episode_ids: Vec, }, + #[error( + "resolving memory {resolver_id} shares no subject or thread with {target_id}; the matter leaves where things stand, but the settling will not come to mind on meeting the person" + )] + ResolverWithoutSharedSubjectOrThread { + resolver_id: MemoryId, + target_id: MemoryId, + }, + #[error( + "scene participant key {participant_id} occurs more than once; the person is stored and reported repeatedly as given" + )] + RepeatedSceneParticipant { participant_id: MemoryId }, } #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] diff --git a/src/memory.rs b/src/memory.rs index 413ed445..b4015119 100644 --- a/src/memory.rs +++ b/src/memory.rs @@ -1050,6 +1050,484 @@ mod tests { .any(|thread| thread.id == fixtures.soft_thread.id)); } + fn warning_time() -> chrono::DateTime { + chrono::DateTime::parse_from_rfc3339("2026-09-20T12:00:00Z") + .unwrap() + .with_timezone(&chrono::Utc) + } + + fn warning_input(offset: u128) -> RememberInput { + let mut episode = EpisodeDraft::new("A recorded occasion"); + episode.id = Some(Uuid::from_u128(offset)); + episode.scene = Some(Scene::at(warning_time())); + episode.created_at = Some(warning_time()); + let mut observation = + ObservationDraft::new(Uuid::from_u128(offset), "The observed details"); + observation.id = Some(Uuid::from_u128(offset + 1)); + observation.created_at = Some(warning_time()); + RememberInput::new("The observed details") + .with_episode(episode) + .with_observation(observation) + } + + fn warning_entity() -> EntityDraft { + let mut draft = EntityDraft::new(); + draft.id = Some(Uuid::from_u128(10)); + draft.created_at = Some(warning_time()); + draft + } + + fn warning_memory( + id: u128, + kind: DerivedType, + text: &str, + subjects: Vec, + ) -> DerivedMemoryDraft { + let mut draft = DerivedMemoryDraft::new(kind, text); + draft.id = Some(Uuid::from_u128(id)); + draft.entity_ids = subjects; + draft.created_at = Some(warning_time()); + draft.updated_at = Some(warning_time()); + draft + } + + fn warning_link() -> MemoryLinkDraft { + let mut draft = MemoryLinkDraft::new( + ObjectType::DerivedMemory, + Uuid::from_u128(40), + RelationType::Resolves, + ObjectType::DerivedMemory, + Uuid::from_u128(30), + ); + draft.id = Some(Uuid::from_u128(50)); + draft.created_at = Some(warning_time()); + draft.schema_version = Some(DEFAULT_SCHEMA_VERSION.to_owned()); + draft + } + + #[tokio::test] + async fn resolver_warnings_use_plan_and_stored_subjects_and_threads() { + for stored in ["neither", "target", "resolver", "both"] { + for shared in ["none", "subject", "thread"] { + let memory = injected_memory().await; + let mut target = warning_memory( + 30, + DerivedType::OpenLoop, + "A matter for Alice", + vec![Uuid::from_u128(10)], + ); + let mut resolver = warning_memory( + 40, + DerivedType::Claim, + "The matter was settled", + if shared == "subject" { + vec![Uuid::from_u128(10)] + } else { + vec![] + }, + ); + if shared == "thread" { + target.thread_ids.push(Uuid::from_u128(60)); + resolver.thread_ids.push(Uuid::from_u128(60)); + } + let mut thread = MemoryThreadDraft::new("A shared matter", "The conversation"); + thread.id = Some(Uuid::from_u128(60)); + thread.created_at = Some(warning_time()); + thread.updated_at = Some(warning_time()); + thread.last_touched_at = Some(warning_time()); + let input = warning_input(100) + .with_entity(warning_entity()) + .with_memory_thread(thread) + .with_derived_memory(target) + .with_derived_memory(resolver) + .with_memory_link(warning_link()); + let mut plan = input.prepare_write_plan(&RememberPlanDefaults::fixed( + "resolver-warning", + warning_time(), + )); + if stored != "neither" { + let stored_ids = match stored { + "target" => vec![Uuid::from_u128(30)], + "resolver" => vec![Uuid::from_u128(40)], + _ => vec![Uuid::from_u128(30), Uuid::from_u128(40)], + }; + let mut seed = plan.clone(); + seed.candidates.retain(|candidate| match candidate { + MemoryCandidate::DerivedMemory(c) => { + stored_ids.contains(&c.draft.id.unwrap()) + } + MemoryCandidate::MemoryLink(_) + | MemoryCandidate::VectorIndex(_) + | MemoryCandidate::StatsUpdate(_) => false, + _ => true, + }); + memory.commit(seed, CommitOptions::default()).await.unwrap(); + plan.candidates.retain(|candidate| match candidate { + MemoryCandidate::DerivedMemory(c) => { + !stored_ids.contains(&c.draft.id.unwrap()) + } + MemoryCandidate::VectorIndex(c) => !stored_ids.contains(&c.target.id), + MemoryCandidate::StatsUpdate(c) => !stored_ids.contains(&c.subject.id), + _ => true, + }); + if stored == "both" { + plan.candidates + .retain(|c| matches!(c, MemoryCandidate::MemoryLink(_))); + } + } + // Endpoints need not precede their link in a caller-built plan. + plan.candidates.reverse(); + let expected = if shared == "none" { + vec![ + CandidateValidationIssue::ResolverWithoutSharedSubjectOrThread { + resolver_id: Uuid::from_u128(40), + target_id: Uuid::from_u128(30), + }, + ] + } else { + vec![] + }; + let validations = memory.validate_plan(&plan).await.unwrap(); + assert!(validations + .iter() + .all(|v| v.status == CandidateValidationStatus::Valid && v.errors.is_empty())); + let index = plan + .candidates + .iter() + .position(|c| matches!(c, MemoryCandidate::MemoryLink(_))) + .unwrap(); + assert_eq!(validations[index].candidate_index, index); + assert_eq!( + validations[index].candidate_kind, + MemoryCandidateKind::MemoryLink + ); + assert_eq!(validations[index].warnings, expected, "{stored}/{shared}"); + let outcome = memory + .commit(plan.clone(), CommitOptions::default()) + .await + .unwrap(); + assert_eq!( + memory.commit(plan, CommitOptions::default()).await.unwrap(), + outcome + ); + assert_eq!( + outcome + .diagnostics + .validations + .iter() + .flat_map(|v| &v.warnings) + .cloned() + .collect::>(), + expected + ); + assert_warning_projection(&outcome, &expected); + if stored == "neither" { + let remembered = injected_memory().await; + let outcome = remembered + .remember(input, RememberOptions::default()) + .await + .unwrap(); + assert_eq!( + outcome + .diagnostics + .validations + .iter() + .flat_map(|v| &v.warnings) + .cloned() + .collect::>(), + expected + ); + assert_warning_projection(&outcome, &expected); + remembered.close().await.unwrap(); + } + memory.close().await.unwrap(); + } + } + } + + #[tokio::test] + async fn resolver_warnings_ignore_other_relations_and_unavailable_or_ineligible_endpoints() { + let memory = injected_memory().await; + let input = warning_input(100) + .with_entity(warning_entity()) + .with_derived_memory(warning_memory( + 30, + DerivedType::Commitment, + "A promise for Alice", + vec![Uuid::from_u128(10)], + )) + .with_derived_memory(warning_memory( + 40, + DerivedType::Claim, + "The promise was fulfilled", + vec![], + )) + .with_memory_link(warning_link()); + let base = input.prepare_write_plan(&RememberPlanDefaults::fixed( + "resolver-edges", + warning_time(), + )); + for case in [ + "fulfills", + "other", + "self", + "missing_target", + "missing_resolver", + "episode_target", + "episode_resolver", + ] { + let mut plan = base.clone(); + let index = plan + .candidates + .iter() + .position(|c| matches!(c, MemoryCandidate::MemoryLink(_))) + .unwrap(); + let MemoryCandidate::MemoryLink(candidate) = &mut plan.candidates[index] else { + unreachable!() + }; + match case { + "fulfills" => candidate.draft.relation = RelationType::FulfillsCommitment, + "other" => candidate.draft.relation = RelationType::AssociatedWith, + "self" => candidate.draft.to_id = candidate.draft.from_id, + "missing_target" => candidate.draft.to_id = Uuid::from_u128(999), + "missing_resolver" => candidate.draft.from_id = Uuid::from_u128(999), + "episode_target" => { + candidate.draft.to_type = ObjectType::Episode; + candidate.draft.to_id = Uuid::from_u128(100); + } + "episode_resolver" => { + candidate.draft.from_type = ObjectType::Episode; + candidate.draft.from_id = Uuid::from_u128(100); + } + _ => unreachable!(), + } + let validations = memory.validate_plan(&plan).await.unwrap(); + let validation = &validations[index]; + if case == "fulfills" { + assert_eq!( + validation.warnings, + vec![ + CandidateValidationIssue::ResolverWithoutSharedSubjectOrThread { + resolver_id: Uuid::from_u128(40), + target_id: Uuid::from_u128(30) + } + ] + ); + let issue = serde_json::to_value(&validation.warnings[0]).unwrap(); + assert_eq!(issue["kind"], "resolver_without_shared_subject_or_thread"); + assert_eq!(issue["resolver_id"], Uuid::from_u128(40).to_string()); + assert_eq!(issue["target_id"], Uuid::from_u128(30).to_string()); + let mut fulfilled = input.clone(); + fulfilled.memory_link_drafts[0].relation = RelationType::FulfillsCommitment; + let outcome = memory + .remember(fulfilled, RememberOptions::default()) + .await + .unwrap(); + assert_eq!( + outcome + .diagnostics + .validations + .iter() + .flat_map(|v| &v.warnings) + .cloned() + .collect::>(), + validation.warnings + ); + assert_warning_projection(&outcome, &validation.warnings); + } else { + assert!(validation.warnings.is_empty(), "{case}"); + } + match case { + "self" => assert!(matches!( + validation.errors.as_slice(), + [CandidateValidationIssue::SelfLink { .. }] + )), + "missing_target" | "missing_resolver" => assert!(matches!( + validation.errors.as_slice(), + [CandidateValidationIssue::UnknownObjectRef { .. }] + )), + _ => assert_eq!( + validation.status, + CandidateValidationStatus::Valid, + "{case}" + ), + } + } + memory.close().await.unwrap(); + } + + #[tokio::test] + async fn repeated_participant_warnings_preserve_scenes_links_and_replays() { + let cases = [ + (vec![Some(10), Some(10)], vec![10]), + (vec![Some(10), Some(10), Some(10)], vec![10]), + (vec![Some(20), Some(10), Some(20), Some(10)], vec![20, 10]), + (vec![Some(10), None], vec![]), + (vec![None, None], vec![]), + (vec![], vec![]), + ]; + for episode_only in [false, true] { + for (keys, repeated) in &cases { + let memory = injected_memory().await; + let mut other = warning_entity(); + other.id = Some(Uuid::from_u128(20)); + memory + .remember( + warning_input(100) + .with_entity(warning_entity()) + .with_entity(other), + RememberOptions::default(), + ) + .await + .unwrap(); + let mut input = warning_input(300); + let mut scene = Scene::at(warning_time()); + scene.participants = keys + .iter() + .map(|key| SceneParticipant { + key: key.map(Uuid::from_u128), + name: None, + description: Some("A familiar face".to_owned()), + }) + .collect(); + input.episode_drafts[0].scene = Some(scene.clone()); + let mut plan = input.prepare_write_plan(&RememberPlanDefaults::fixed( + "participant-warning", + warning_time(), + )); + if episode_only { + plan.candidates + .retain(|c| matches!(c, MemoryCandidate::Episode(_))); + } + let expected = repeated + .iter() + .map(|id| CandidateValidationIssue::RepeatedSceneParticipant { + participant_id: Uuid::from_u128(*id), + }) + .collect::>(); + let validations = memory.validate_plan(&plan).await.unwrap(); + assert_eq!(validations[0].candidate_kind, MemoryCandidateKind::Episode); + assert_eq!(validations[0].candidate_index, 0); + assert_eq!(validations[0].warnings, expected); + assert!(validations + .iter() + .all(|v| v.status == CandidateValidationStatus::Valid)); + let outcome = if episode_only { + memory + .commit(plan.clone(), CommitOptions::default()) + .await + .unwrap() + } else { + memory + .remember(input, RememberOptions::default()) + .await + .unwrap() + }; + assert_eq!( + outcome + .diagnostics + .validations + .iter() + .flat_map(|v| &v.warnings) + .cloned() + .collect::>(), + expected + ); + assert_warning_projection(&outcome, &expected); + for (warning, id) in expected.iter().zip(repeated) { + assert_eq!( + serde_json::to_value(warning).unwrap(), + serde_json::json!({"kind":"repeated_scene_participant","participant_id":Uuid::from_u128(*id)}) + ); + } + let graph = memory.memory_composition.graph_store.as_ref(); + let objects = graph + .query_objects(&GraphObjectQuery::by_types(vec![ObjectType::Episode], None)) + .await + .unwrap(); + let episode = objects + .iter() + .find_map(|o| match o { + MemoryObject::Episode(e) if e.id == Uuid::from_u128(300) => Some(e), + _ => None, + }) + .unwrap(); + assert_eq!(episode.scene, scene); + let links = graph + .query_links_by_ids(&outcome.persisted_link_ids) + .await + .unwrap(); + let mut distinct = keys.iter().flatten().copied().collect::>(); + distinct.sort_unstable(); + distinct.dedup(); + assert_eq!(links.len(), distinct.len()); + for key in distinct { + assert_eq!( + links + .iter() + .filter(|l| l.to_id == Uuid::from_u128(key) + && l.to_type == ObjectType::Entity + && l.relation + == if episode_only { + RelationType::Involves + } else { + RelationType::Mentions + }) + .count(), + 1 + ); + } + if episode_only { + assert_eq!( + memory.commit(plan, CommitOptions::default()).await.unwrap(), + outcome + ); + } else { + // A prepared standard-path write is equally replayable; its generated link ids are fixed. + let separate = injected_memory().await; + let mut other = warning_entity(); + other.id = Some(Uuid::from_u128(20)); + separate + .remember( + warning_input(100) + .with_entity(warning_entity()) + .with_entity(other), + RememberOptions::default(), + ) + .await + .unwrap(); + let first = separate + .commit(plan.clone(), CommitOptions::default()) + .await + .unwrap(); + assert_eq!( + separate + .commit(plan, CommitOptions::default()) + .await + .unwrap(), + first + ); + separate.close().await.unwrap(); + } + memory.close().await.unwrap(); + } + } + } + + fn assert_warning_projection(outcome: &RememberOutcome, expected: &[CandidateValidationIssue]) { + let messages = outcome + .diagnostics + .messages + .iter() + .filter(|m| m.code == RememberDiagnosticCode::WritePlanValidationWarning) + .collect::>(); + assert_eq!(messages.len(), expected.len()); + for (message, warning) in messages.into_iter().zip(expected) { + assert_eq!(message.severity, DiagnosticSeverity::Warning); + assert_eq!(message.message, warning.to_string()); + } + } + async fn injected_memory() -> CharacterMemory { CharacterMemory::from_parts( Box::new(in_memory_graph_store()), diff --git a/src/usecases/write_planning.rs b/src/usecases/write_planning.rs index 5e394963..ab4b577f 100644 --- a/src/usecases/write_planning.rs +++ b/src/usecases/write_planning.rs @@ -746,6 +746,7 @@ struct PlanValidationContext { refs_requiring_graph: HashSet, existing_refs: HashSet, episode_content_by_id: HashMap, + derived_subjects_and_threads: HashMap, Vec)>, } impl PlanValidationContext { @@ -755,6 +756,7 @@ impl PlanValidationContext { refs_requiring_graph: HashSet::new(), existing_refs: HashSet::new(), episode_content_by_id: HashMap::new(), + derived_subjects_and_threads: HashMap::new(), }; for candidate in &plan.candidates { @@ -796,6 +798,14 @@ impl PlanValidationContext { if let Some(id) = candidate.draft.id { self.plan_refs .insert(MemoryObjectRef::from_id_type(id, ObjectType::DerivedMemory)); + self.derived_subjects_and_threads + .entry(id) + .or_insert_with(|| { + ( + candidate.draft.entity_ids.clone(), + candidate.draft.thread_ids.clone(), + ) + }); } } MemoryCandidate::MemoryLink(candidate) => { @@ -886,6 +896,11 @@ impl PlanValidationContext { fn add_existing_object(&mut self, object: &MemoryObject) { self.existing_refs.insert(object.object_ref()); + if let MemoryObject::DerivedMemory(memory) = object { + self.derived_subjects_and_threads + .entry(memory.id) + .or_insert_with(|| (memory.entity_ids.clone(), memory.thread_ids.clone())); + } } fn validate_candidate(&self, index: usize, candidate: &MemoryCandidate) -> CandidateValidation { @@ -1073,9 +1088,63 @@ impl PlanValidationContext { if let Some(warning) = self.echo_surface_warning(candidate) { validation.warnings.push(warning); } + match candidate { + MemoryCandidate::MemoryLink(candidate) => { + if let Some(warning) = self.resolver_warning(&candidate.draft) { + validation.warnings.push(warning); + } + } + MemoryCandidate::Episode(candidate) => { + if let Some(scene) = &candidate.draft.scene { + let mut seen = HashSet::new(); + let mut warned = HashSet::new(); + for participant_id in scene.participant_keys() { + if !seen.insert(participant_id) && warned.insert(participant_id) { + validation.warnings.push( + CandidateValidationIssue::RepeatedSceneParticipant { + participant_id, + }, + ); + } + } + } + } + _ => {} + } validation } + fn resolver_warning(&self, link: &MemoryLinkDraft) -> Option { + if !matches!( + link.relation, + RelationType::Resolves | RelationType::FulfillsCommitment + ) || link.from_type != ObjectType::DerivedMemory + || link.to_type != ObjectType::DerivedMemory + || link.from_id == link.to_id + { + return None; + } + let (resolver_subjects, resolver_threads) = + self.derived_subjects_and_threads.get(&link.from_id)?; + let (target_subjects, target_threads) = + self.derived_subjects_and_threads.get(&link.to_id)?; + if resolver_subjects + .iter() + .any(|id| target_subjects.contains(id)) + || resolver_threads + .iter() + .any(|id| target_threads.contains(id)) + { + return None; + } + Some( + CandidateValidationIssue::ResolverWithoutSharedSubjectOrThread { + resolver_id: link.from_id, + target_id: link.to_id, + }, + ) + } + fn echo_surface_warning( &self, candidate: &MemoryCandidate, From 0d6502abf54a1360128cd380865aeffced2fcda0 Mon Sep 17 00:00:00 2001 From: ebigunso Date: Wed, 23 Sep 2026 00:52:03 +0900 Subject: [PATCH 2/3] =?UTF-8?q?=E2=9A=A0=EF=B8=8F=20Explain=20duplicate=20?= =?UTF-8?q?participants=20through=20recall=20consequences?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/domain/write_validation.rs | 2 +- src/memory.rs | 26 -------------------------- 2 files changed, 1 insertion(+), 27 deletions(-) diff --git a/src/domain/write_validation.rs b/src/domain/write_validation.rs index 1779f228..3f6ae49d 100644 --- a/src/domain/write_validation.rs +++ b/src/domain/write_validation.rs @@ -132,7 +132,7 @@ pub enum CandidateValidationIssue { target_id: MemoryId, }, #[error( - "scene participant key {participant_id} occurs more than once; the person is stored and reported repeatedly as given" + "scene participant key {participant_id} is listed more than once; recall will report the person present twice and their words count twice in participant search" )] RepeatedSceneParticipant { participant_id: MemoryId }, } diff --git a/src/memory.rs b/src/memory.rs index b4015119..fa759cc3 100644 --- a/src/memory.rs +++ b/src/memory.rs @@ -1482,32 +1482,6 @@ mod tests { memory.commit(plan, CommitOptions::default()).await.unwrap(), outcome ); - } else { - // A prepared standard-path write is equally replayable; its generated link ids are fixed. - let separate = injected_memory().await; - let mut other = warning_entity(); - other.id = Some(Uuid::from_u128(20)); - separate - .remember( - warning_input(100) - .with_entity(warning_entity()) - .with_entity(other), - RememberOptions::default(), - ) - .await - .unwrap(); - let first = separate - .commit(plan.clone(), CommitOptions::default()) - .await - .unwrap(); - assert_eq!( - separate - .commit(plan, CommitOptions::default()) - .await - .unwrap(), - first - ); - separate.close().await.unwrap(); } memory.close().await.unwrap(); } From 216487561a8ee2b6225fcb79081bae63610690f0 Mon Sep 17 00:00:00 2001 From: ebigunso Date: Wed, 23 Sep 2026 00:56:15 +0900 Subject: [PATCH 3/3] =?UTF-8?q?=F0=9F=93=9D=20The=20write-warnings=20plan?= =?UTF-8?q?=20is=20complete?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5.1 --- .../{active => completed}/v0-2-write-warnings-plan.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) rename docs/coding-agent/plans/{active => completed}/v0-2-write-warnings-plan.md (96%) diff --git a/docs/coding-agent/plans/active/v0-2-write-warnings-plan.md b/docs/coding-agent/plans/completed/v0-2-write-warnings-plan.md similarity index 96% rename from docs/coding-agent/plans/active/v0-2-write-warnings-plan.md rename to docs/coding-agent/plans/completed/v0-2-write-warnings-plan.md index f2e0b0ea..1b37f4ac 100644 --- a/docs/coding-agent/plans/active/v0-2-write-warnings-plan.md +++ b/docs/coding-agent/plans/completed/v0-2-write-warnings-plan.md @@ -1,6 +1,6 @@ # Plan: the library tells the writer when what it is about to store will not do what the writer means -- status: in-progress +- status: completed - generated: 2026-09-22 - last_updated: 2026-09-22 - work_type: code @@ -102,6 +102,7 @@ One crate, one task, one PR stacked on the top of the library stack as Integrati - One PR that reverts on its own; no stored format changes, so a revert leaves nothing behind. ## Progress Log (append-only) +- 2026-09-22: the one task is done at 0d6502a (PR 142): Tier D approved at e1ff33e (nine-case observer red at the parent, green at the tip, outcomes and links equal), Tier A approved with two changes applied. - (none yet) @@ -119,6 +120,11 @@ One crate, one task, one PR stacked on the top of the library stack as Integrati - Tradeoffs considered: a margin on the late time was rejected before as an unmeasured number, and no margin survives the reference-time argument. - User approval: decided under the standing instruction and logged for presentation. - Record proposed: none. +- 2026-09-22 Decision: the plan is complete; its completion audit is the Tier A implementation review. + - Trigger / new insight: the plan has one task and changes no recall; the Tier A review judged every added function, test and README line (two warnings earn their place; one redundant test block deleted; the warning text states its consequence). + - Plan delta (what changed): none; the measurement item stays an explicit exemption, with the driver's warning drift test as the evidence. + - User approval: decided under the standing instruction and logged for presentation. + - Record proposed: none. ## Notes - Risks: an application that already writes resolutions with no shared subject by design (a resolver about a thread only, say) gets a warning on each; the warning is accurate (the state route brings current memories by subject) and the thread intersection quiets it when the thread is shared. A test fixture that repeats a key gains a warning and no other change.