From aeeb36ad2a7cba5be0edd4240a2700cbb37f3751 Mon Sep 17 00:00:00 2001 From: Eureka Date: Wed, 11 Nov 2026 00:06:27 +0000 Subject: [PATCH] Desk answer: delete the replayed flag nobody read; tests assert that a replay commits nothing Co-Authored-By: Claude Sonnet 5.5 --- crates/huginn-daemon/src/daemon.rs | 2 +- crates/huginn-mind/src/desk_wire.rs | 97 ++++++++++++----------- crates/huginn-mind/src/desk_wire/reads.rs | 4 +- 3 files changed, 53 insertions(+), 50 deletions(-) diff --git a/crates/huginn-daemon/src/daemon.rs b/crates/huginn-daemon/src/daemon.rs index f66a9e5..79c38e5 100644 --- a/crates/huginn-daemon/src/daemon.rs +++ b/crates/huginn-daemon/src/daemon.rs @@ -243,7 +243,7 @@ impl> Daemon { } match request { DeskRequest::Answer { subject, authority, deciders, request, .. } => { - self.mind.desk_answer(&deciders, subject, authority, request, now).0 + self.mind.desk_answer(&deciders, subject, authority, request, now) } DeskRequest::Forks { deciders, status, cursor, limit, .. } => self.mind.desk_forks(&deciders, status, cursor, limit), DeskRequest::Fork { deciders, fork_id, .. } => self.mind.desk_fork(&deciders, &fork_id), diff --git a/crates/huginn-mind/src/desk_wire.rs b/crates/huginn-mind/src/desk_wire.rs index 65fc77a..3c2c956 100644 --- a/crates/huginn-mind/src/desk_wire.rs +++ b/crates/huginn-mind/src/desk_wire.rs @@ -483,9 +483,8 @@ fn showable(question: &PipelineQuestion, deciders: &[Short]) -> bool { impl Mind { /// One desk answer from the door: the wire request converted, the question gated, the answer - /// decided and stored (or replayed) by `answer_desk`, and the stored answer projected with the - /// derived count at the head it was read at. The flag is `true` when nothing was committed (a - /// replay); it is for the door and is never encoded. `subject` and `authority` are the verified + /// decided and stored (or committed) by `answer_desk`, and the stored answer projected with the + /// derived count at the head it was read at. `subject` and `authority` are the verified /// caller's, `deciders` the door's list. pub fn desk_answer( &mut self, @@ -494,10 +493,10 @@ impl Mind { authority: DeskAuthority, request: ForkAnswerRequest, now: DateTime, - ) -> (DeskResponse, bool) { + ) -> DeskResponse { match self.desk_response(deciders, subject, authority, &request, now) { Ok(done) => done, - Err(refused) => (door_refusal(refused), false), + Err(refused) => door_refusal(refused), } } @@ -508,10 +507,10 @@ impl Mind { authority: DeskAuthority, request: &ForkAnswerRequest, now: DateTime, - ) -> Result<(DeskResponse, bool), MindRefusal> { + ) -> Result { let sent = match request_of(request) { Ok(sent) => sent, - Err(refused) => return Ok((DeskResponse::NotStored(refused), false)), + Err(refused) => return Ok(DeskResponse::NotStored(refused)), }; let image = self.image()?; let docs = Docs::at(image, image.head); @@ -520,12 +519,11 @@ impl Mind { let unshowable = bound_answer(&docs, &subject, &sent.request_id).is_none() && matches!(docs.find(PipelineKind::Question, &sent.fork.0), Some(PipelineDocument::Question(question)) if !showable(question, deciders)); if unshowable { - return Ok((DeskResponse::NotStored(fork_unknown()), false)); + return Ok(DeskResponse::NotStored(fork_unknown())); } let stamp = DeskStamp { answerer: subject, authority, deciders: deciders.to_vec() }; - let (key, replayed) = match self.answer_desk(stamp, sent, now) { - DeskAnswerOutcome::Stored { answer, .. } => (answer.0, false), - DeskAnswerOutcome::Replayed { answer, .. } => (answer.0, true), + let key = match self.answer_desk(stamp, sent, now) { + DeskAnswerOutcome::Stored { answer, .. } | DeskAnswerOutcome::Replayed { answer, .. } => answer.0, DeskAnswerOutcome::Refused(refused) => return Err(refused), }; let image = self.image()?; @@ -533,7 +531,7 @@ impl Mind { let Some(PipelineDocument::DeskAnswer(stored)) = docs.find(PipelineKind::DeskAnswer, &key) else { return Err(unreadable_key()) }; let fork = stored.fork.0.as_str(); let counted_answer_id = counted_answer(&docs, fork).map(|counted| answer_seq(fork, &counted).map(n_text).ok_or_else(unreadable_key)).transpose()?; - Ok((DeskResponse::Answered { answer: project(&key, stored)?, counted_answer_id, as_of: image.head }, replayed)) + Ok((DeskResponse::Answered { answer: project(&key, stored)?, counted_answer_id, as_of: image.head })) } } @@ -621,8 +619,12 @@ mod tests { list.iter().map(|who| s(who)).collect() } + /// The response and whether the call committed anything: a replay, a refused body and a gate + /// refusal store nothing, so the head does not move. fn ask_as(mind: &mut Mind, who: &str, list: &[&str], request: ForkAnswerRequest) -> (DeskResponse, bool) { - mind.desk_answer(&deciders(list), s(who), DeskAuthority::Member, request, now()) + let before = head(mind); + let response = mind.desk_answer(&deciders(list), s(who), DeskAuthority::Member, request, now()); + (response, head(mind) != before) } fn ask(mind: &mut Mind, who: &str, request: ForkAnswerRequest) -> (DeskResponse, bool) { @@ -883,9 +885,9 @@ mod tests { assert_eq!((answer.outcome.as_str(), answer.retracts.as_deref(), counted.as_deref(), as_of), ("Retraction", Some("n1"), Some("n2"), head(&mind))); // The first request replayed: its stored word is still Counted, the count is read now. let before = head(&mind); - let (response, replayed) = ask(&mut mind, "x", first); + let (response, committed) = ask(&mut mind, "x", first); let (answer, counted, as_of) = answered(response); - assert!(replayed); + assert!(!committed); assert_eq!((answer.outcome.as_str(), answer.answer_id.as_str(), counted.as_deref(), as_of), ("Counted", "n1", Some("n2"), before)); assert_eq!(head(&mind), before); // The second request replayed now counts, and its stored word is still Revision. @@ -1021,14 +1023,14 @@ mod tests { ]; for (form, request) in forms.into_iter().enumerate() { let stored = stored_count(&mind, FORK); - let (response, replayed) = ask(&mut mind, "x", request.clone()); - assert!(!replayed); + let (response, committed) = ask(&mut mind, "x", request.clone()); + assert!(committed); let (answer, _, _) = answered(response.clone()); assert_eq!((answer.outcome.as_str(), answer.code), ("Refused", Some(ForkAnswerCode::BadRetraction)), "form {form}"); assert_eq!(stored_count(&mind, FORK), stored + 1, "form {form}"); // The refusal is stored and bound: the same bytes replay, with no write. let before = head(&mind); - assert_eq!(ask(&mut mind, "x", request), (response, true), "form {form}"); + assert_eq!(ask(&mut mind, "x", request), (response, false), "form {form}"); assert_eq!(head(&mind), before); } // (i) and (iii) with equal answer and target differ only in `retracted`: one id, two bodies. @@ -1154,14 +1156,14 @@ mod tests { fn a_stale_answer_carries_no_card() { let mut mind = desk(); let stale = wire(FORK, &"e".repeat(64)); - let (fresh, replayed) = ask(&mut mind, "x", stale.clone()); - assert!(!replayed); + let (fresh, committed) = ask(&mut mind, "x", stale.clone()); + assert!(committed); let (answer, _, _) = answered(fresh.clone()); assert_eq!((answer.outcome.as_str(), answer.code), ("Refused", Some(ForkAnswerCode::StaleFork))); // After the fork closed, the retry still answers the stored refusal, with nothing added. close(&mut mind, FORK, "RQ1", "A"); - let (again, replayed) = ask(&mut mind, "x", stale); - assert!(replayed); + let (again, committed) = ask(&mut mind, "x", stale); + assert!(!committed); let (again_answer, _, _) = answered(again.clone()); assert_eq!(again_answer, answer); for response in [&fresh, &again] { @@ -1176,7 +1178,7 @@ mod tests { let request = wire(FORK, &hash); let (fresh, first) = ask(&mut mind, "x", request.clone()); let (replay, second) = ask(&mut mind, "x", request); - assert_eq!((first, second), (false, true)); + assert_eq!((first, second), (true, false)); assert_eq!(encode(&fresh), encode(&replay)); assert_eq!(stored_count(&mind, FORK), 1); } @@ -1225,17 +1227,17 @@ mod tests { let mut mind = desk(); let hash = hash_of(&mind, FORK); let request = wire(FORK, &hash); - let (stored, replayed) = ask(&mut mind, "x", request.clone()); - assert!(!replayed); + let (stored, committed) = ask(&mut mind, "x", request.clone()); + assert!(committed); let before = head(&mind); // The list Bifrost passes is empty now; the stored request still gets its stored answer. - let (again, replayed) = ask_as(&mut mind, "x", &[], request.clone()); - assert!(replayed); + let (again, committed) = ask_as(&mut mind, "x", &[], request.clone()); + assert!(!committed); assert_eq!(encode(&again), encode(&stored)); assert_eq!(head(&mind), before); // Another subject with the same bytes has stored nothing: the gate refuses it. - let (other, replayed) = ask_as(&mut mind, "y", &[], request); - assert_eq!((not_stored(other).code, replayed), (ForkRefusalCode::ForkUnknown, false)); + let (other, committed) = ask_as(&mut mind, "y", &[], request); + assert_eq!((not_stored(other).code, committed), (ForkRefusalCode::ForkUnknown, false)); // And a request nobody stored is fork_unknown while the list is empty. let fresh = wire(FORK, &hash); assert_eq!(not_stored(ask_as(&mut mind, "x", &[], fresh).0).code, ForkRefusalCode::ForkUnknown); @@ -1243,22 +1245,22 @@ mod tests { // A stored refusal and a stored revision replay the same way: a stale hash is stored as a // refusal, a second answer by the same subject as a revision. let stale = wire(FORK, "0".repeat(hash.len()).as_str()); - let (refused, replayed) = ask(&mut mind, "x", stale.clone()); - assert!(!replayed); + let (refused, committed) = ask(&mut mind, "x", stale.clone()); + assert!(committed); assert_eq!(answered(refused.clone()).0.outcome, "Refused"); let revision = wire(FORK, &hash); - let (revised, replayed) = ask(&mut mind, "x", revision.clone()); - assert!(!replayed); + let (revised, committed) = ask(&mut mind, "x", revision.clone()); + assert!(committed); assert_eq!(answered(revised.clone()).0.outcome, "Revision"); let before = head(&mind); for (request, stored) in [(stale, refused), (revision, revised)] { - let (again, replayed) = ask_as(&mut mind, "x", &[], request.clone()); - assert!(replayed); + let (again, committed) = ask_as(&mut mind, "x", &[], request.clone()); + assert!(!committed); // `as_of` is the head the replay was read at, so the stored answer is what is compared. assert_eq!(encode(&answered(again).0), encode(&answered(stored).0)); assert_eq!(head(&mind), before); - let (other, replayed) = ask_as(&mut mind, "w", &[], request); - assert_eq!((not_stored(other).code, replayed), (ForkRefusalCode::ForkUnknown, false)); + let (other, committed) = ask_as(&mut mind, "w", &[], request); + assert_eq!((not_stored(other).code, committed), (ForkRefusalCode::ForkUnknown, false)); assert_eq!(head(&mind), before); } } @@ -1322,16 +1324,16 @@ mod tests { let (first, _) = ask(&mut mind, "x", request.clone()); // V12: the same bytes replay. let before = head(&mind); - assert_eq!(ask(&mut mind, "x", request.clone()), (first, true)); + assert_eq!(ask(&mut mind, "x", request.clone()), (first, false)); assert_eq!(head(&mind), before); // V13: only answered_at changed is a conflict, and nothing is stored. let moved = ForkAnswerRequest { answered_at: "2026-11-10T12:00:01Z".into(), ..request.clone() }; assert_eq!(not_stored(ask(&mut mind, "x", moved).0).code, ForkRefusalCode::RequestIdConflict); assert_eq!((head(&mind), stored_count(&mind, FORK)), (before, 1)); // V14: another subject with identical bytes is its own request: its own answer id, stamped with it. - let (other, replayed) = ask(&mut mind, "y", request.clone()); + let (other, committed) = ask(&mut mind, "y", request.clone()); let (other, _, _) = answered(other); - assert!(!replayed); + assert!(committed); assert_eq!((other.answer_id.as_str(), other.answerer.as_str(), other.outcome.as_str(), other.request_id.as_str()), ("n2", "y", "Revision", request.request_id.as_str())); let (original, _, _) = answered(ask(&mut mind, "x", request.clone()).0); assert_eq!((original.answer_id.as_str(), original.answerer.as_str()), ("n1", "x")); @@ -1489,7 +1491,7 @@ mod tests { let authority = serde_json::from_value(server["authority"].clone()).unwrap(); let at = DateTime::parse_from_rfc3339(server["received_at"].as_str().unwrap()).unwrap().to_utc(); let answerer = s(server["answerer"].as_str().unwrap()); - let (mine, _) = mind.desk_answer(&deciders(&list), answerer, authority, request, at); + let mine = mind.desk_answer(&deciders(&list), answerer, authority, request, at); if code == Some("unavailable") { continue; // the door went down after the Mind stored: its answer was never seen } @@ -1545,9 +1547,9 @@ mod tests { let mut mind = desk(); let hash = hash_of(&mind, FORK); // The operator's own account, off the list, is no decider; a member on the list is. - let off = mind.desk_answer(&deciders(&["x"]), s("op"), DeskAuthority::Operator, wire(FORK, &hash), now()).0; + let off = mind.desk_answer(&deciders(&["x"]), s("op"), DeskAuthority::Operator, wire(FORK, &hash), now()); assert_eq!(answered(off).0.code, Some(ForkAnswerCode::NotADecider)); - let on = mind.desk_answer(&deciders(&["m"]), s("m"), DeskAuthority::Member, wire(FORK, &hash), now()).0; + let on = mind.desk_answer(&deciders(&["m"]), s("m"), DeskAuthority::Member, wire(FORK, &hash), now()); let (answer, _, _) = answered(on); assert_eq!((answer.outcome.as_str(), answer.authority, answer.answerer.as_str()), ("Counted", DeskAuthority::Member, "m")); } @@ -1587,8 +1589,9 @@ mod tests { } // 64 deciders through to the Mind: the subject is listed and counts. let DeskRequest::Answer { subject, authority, deciders, request, .. } = through_the_door(&desk_requests(INSTANCE, &list_of(64), &hash)[0]).unwrap() else { panic!("not an answer") }; - let (response, replayed) = mind.desk_answer(&deciders, subject, authority, request, now()); - assert!(!replayed); + let before = head(&mind); + let response = mind.desk_answer(&deciders, subject, authority, request, now()); + assert!(head(&mind) > before); assert_eq!(answered(response).0.outcome, "Counted"); let (head_before, stored_before) = (head(&mind), stored_count(&mind, FORK)); @@ -1622,7 +1625,7 @@ mod tests { assert_eq!((head(&mind), stored_count(&mind, FORK)), before); // The Mind's showability gate is the second line: an empty list handed to it directly // still refuses the fork and stores nothing. - let refused = not_stored(mind.desk_answer(&[], s("d0"), DeskAuthority::Member, wire(FORK, &hash), now()).0); + let refused = not_stored(mind.desk_answer(&[], s("d0"), DeskAuthority::Member, wire(FORK, &hash), now())); assert_eq!(refused.code, ForkRefusalCode::ForkUnknown); assert_eq!((head(&mind), stored_count(&mind, FORK)), before); } @@ -1643,6 +1646,6 @@ mod tests { // The same request to the mind it names is answered. let DeskRequest::Answer { mind: named, subject, authority, deciders, request } = through_the_door(&desk_requests(INSTANCE, &list_of(4), &hash)[0]).unwrap() else { panic!("not an answer") }; mind.require_instance(&named).unwrap(); - assert_eq!(answered(mind.desk_answer(&deciders, subject, authority, request, now()).0).0.outcome, "Counted"); + assert_eq!(answered(mind.desk_answer(&deciders, subject, authority, request, now())).0.outcome, "Counted"); } } diff --git a/crates/huginn-mind/src/desk_wire/reads.rs b/crates/huginn-mind/src/desk_wire/reads.rs index 8fbefca..c17a647 100644 --- a/crates/huginn-mind/src/desk_wire/reads.rs +++ b/crates/huginn-mind/src/desk_wire/reads.rs @@ -461,7 +461,7 @@ mod tests { answered_at: "2026-09-16T11:00:00Z".into(), request_id: request_id(), }; - mind.desk_answer(&list(deciders), s(who), DeskAuthority::Member, request, now()).0 + mind.desk_answer(&list(deciders), s(who), DeskAuthority::Member, request, now()) } fn close(mind: &mut Mind, fork: &str, label: &str) { @@ -755,7 +755,7 @@ mod tests { answered_at: "2026-09-16T11:00:00Z".into(), request_id: request_id(), }; - let stored = mind.desk_answer(&list(&LIST), s("x"), DeskAuthority::Member, retract, now()).0; + let stored = mind.desk_answer(&list(&LIST), s("x"), DeskAuthority::Member, retract, now()); assert!(matches!(stored, DeskResponse::Answered { .. })); let (after, _) = entry(mind.desk_fork(&list(&LIST), &fork(1))); assert_eq!(after.record.counted_answer_id.as_deref(), Some("n2"));