Lot 34 : bloc projet figé (PROJ_SNAPSHOT) dans le tour de la façade

PROJ_SNAPSHOT était le dernier réglage qui gardait la conversation au
Go : la façade la tient désormais dans tous les cas (LOKI_CHAT=go reste
la porte de sortie). facadeChatBlocker et la décision prise une fois par
processus disparaissent.

Go (facade_projsnap.go) :
- /internal/v1/turn/view, avec la clé et l'agent : tête du tour portant
  le bloc figé (ou le vivant à rafraîchir), préambule sans date ni
  dossier, et la décision projsnap (rafraîchir, mise à jour à poser,
  instantané à ranger, clé et système du tour) ; dry pour le
  préchauffage ;
- /internal/v1/turn/projsnap/refresh : bloc vivant et instantané qu'il
  devient, pour une compaction en plein tour.

Rust :
- ChatService::assemble_turn, partagé par le tour et la relance du
  builder : instantané rangé avec la discussion, mise à jour posée en tête
  du dernier message utilisateur (prependCtxUpdate), historique nettoyé
  au rafraîchissement ;
- TurnHost::project_refresh après une compaction ou une réduction en
  plein tour (replaceProjectHead) ; l'instantané posé devient celui de la
  discussion ;
- le préchauffage demande la vue en dry et garde les mises à jour tant
  que le bloc n'est pas rafraîchi.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
This commit is contained in:
Claude committed 2026-10-16 03:13:27 +00:00
1 parent ff5c6d4571
commit ad52da18a9
19 files changed
+1063 -171

No files matched your search

+60
View File
@@ -52,6 +52,9 @@ struct Fake {
/// a reçu.
prewarm_match: Mutex<bool>,
sent_keeps: Mutex<Vec<bool>>,
/// PROJ_SNAPSHOT : clé posée pour ce tour ; rafraîchissements demandés.
snap_on: Mutex<bool>,
refreshes: Mutex<usize>,
}
impl Fake {
@@ -173,6 +176,13 @@ impl TurnHost for Fake {
async fn keep_images(&self) -> Option<i64> {
*self.keep.lock().unwrap()
}
async fn project_refresh(&self, mut msgs: Vec<Message>) -> Vec<Message> {
if *self.snap_on.lock().unwrap() {
*self.refreshes.lock().unwrap() += 1;
msgs.insert(0, text_msg("system", "BLOC VIVANT"));
}
msgs
}
fn prewarm_keeps(&self, prefix: &[u64], payload: &Value) -> bool {
// Ce que l'hôte reçoit : les empreintes des messages envoyés et le
// corps de la requête.
@@ -1094,3 +1104,53 @@ async fn prechauffage_garde_par_la_requete_qui_le_prolonge() {
assert_eq!(r.err, None);
assert_eq!(*host.sent_keeps.lock().unwrap(), [false]);
}
/// PROJ_SNAPSHOT : une compaction ou une réduction en plein tour fait
/// rafraîchir le bloc projet (le début du prompt change de toute façon) ;
/// l'historique publié et la requête rejouée le portent. Le retrait d'images
/// gardées, lui, ne touche pas au début du prompt.
#[tokio::test]
async fn bloc_projet_rafraichi_par_la_compaction() {
let host = Fake::new(vec![
Script::Status(400, "the request exceeds the available context size".into()),
answer("après compaction"),
]);
*host.compacted.lock().unwrap() = Some(vec![um("résumé")]);
*host.snap_on.lock().unwrap() = true;
let (r, evs) = run(&host, &config(&[]), &Cancel::never()).await;
assert_eq!(r.err, None, "{:?}", host.bodies.lock().unwrap());
assert_eq!(*host.refreshes.lock().unwrap(), 1);
assert!(evs.iter().any(|e| matches!(e, StreamEvent::NewHistory(h)
if h.first().and_then(|m| m.content.as_ref()) == Some(&json!("BLOC VIVANT")))));
assert_eq!(host.body(1)["messages"][0]["content"], "BLOC VIVANT");
// Réduction forcée (pas de résumé possible) : rafraîchi aussi.
let host = Fake::new(vec![
Script::Status(
400,
"request (400000 tokens) exceeds the available context size (32768 tokens)".into(),
),
answer("ok"),
]);
*host.snap_on.lock().unwrap() = true;
let hist: Vec<Message> = (0..12)
.map(|i| um(&format!("message {i} {}", "x".repeat(400))))
.collect();
let (r, _) = run_hist(&host, &config(&[]), hist).await;
assert_eq!(r.err, None);
assert_eq!(*host.refreshes.lock().unwrap(), 1);
assert!(
host.phases.lock().unwrap().len() == 1,
"résumé tenté puis réduction"
);
assert_eq!(host.body(1)["messages"][0]["content"], "BLOC VIVANT");
// Images gardées retirées au filet réactif : pas de rafraîchissement.
let host = Fake::new(vec![
Script::Status(400, "the request exceeds the available context size".into()),
answer_u("fini", 0),
]);
*host.snap_on.lock().unwrap() = true;
let hist = vec![um("q"), tool_res("[ok]"), kept_relay(1000), um("suite")];
let (r, _) = run_hist(&host, &config(&[]), hist).await;
assert_eq!(r.err, None);
assert_eq!(*host.refreshes.lock().unwrap(), 0);
}