Lot 31 : slots du moteur pour le tour tenu par la façade

SIDE_SLOT et SLOT_PERSIST ne gardent plus la conversation au Go. La
machinerie reste au Go (configuration, requêtes en vol, tampon du slot) ;
la façade la consulte avant chaque requête au moteur local.

Go :
- /internal/v1/turn/slot (facade_slots.go) rend engineSlotFor et, pour
  une étape de tour, recharge d'abord l'état gardé de la discussion
  (slotPersistRestore). La sauvegarde à la bascule voit les étapes de la
  façade : /internal/v1/oai/served marque le slot (engineMarkMain).
- facadeChatBlocker : SIDE_SLOT et SLOT_PERSIST retirés.

Rust :
- TurnHost::engine_slot : l'id_slot posé dans chaque requête (0 pour le
  fil de la discussion, 1 pour la vérification), le ticket « en vol »
  ouvert sur le second slot.
- Cache KV plein en plein calcul (sideSlotPoolError) : rejoué deux fois
  tel quel, sans compaction, puis l'erreur.
- Résumés de compaction comptés en vol comme postSummary : transcription
  sur le second slot, continuation sur le slot 0.
- La passe de vérification est un travail annexe (engineSideJob) : le Go
  efface le slot à la fin s'il a servi (écart du lot 7 levé).

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 02:13:20 +00:00
1 parent 89ed527c96
commit a005626e66
14 files changed
+679 -58

No files matched your search

+22 -1
View File
@@ -127,6 +127,11 @@ pub struct FakeGo {
pub service_os_fail: Mutex<Option<(String, String)>>,
/// /internal/v1/turn/perf reçus (corps entiers).
pub perf_records: Mutex<Vec<serde_json::Value>>,
/// /internal/v1/turn/slot : second slot en service (le fil de la
/// discussion sur le 0, le reste sur le 1 ; sinon aucun) ; demandes
/// reçues (« conv main »).
pub turn_side_live: Mutex<bool>,
pub turn_slots: Mutex<Vec<String>>,
}
impl FakeGo {
@@ -666,10 +671,11 @@ async fn handle(st: Arc<FakeGo>, req: Request<Incoming>) -> Response<Full<Bytes>
let body = req.into_body().collect().await.unwrap().to_bytes();
let v: serde_json::Value = serde_json::from_slice(&body).unwrap();
if let Some(conv) = v["main_conv"].as_str() {
let side = if v["side"] == true { " side" } else { "" };
st.oai_log
.lock()
.unwrap()
.push(format!("begin main_conv={conv}"));
.push(format!("begin main_conv={conv}{side}"));
} else {
st.oai_log.lock().unwrap().push(format!(
"begin read={} side={} post_v1={}",
@@ -681,6 +687,21 @@ async fn handle(st: Arc<FakeGo>, req: Request<Incoming>) -> Response<Full<Bytes>
}
resp(200, r#"{"ticket":7}"#)
}
"/internal/v1/turn/slot" => {
let body = req.into_body().collect().await.unwrap().to_bytes();
let v: serde_json::Value = serde_json::from_slice(&body).unwrap();
let main = v["main"] == true;
st.turn_slots
.lock()
.unwrap()
.push(format!("{} {main}", v["conv"].as_str().unwrap_or("")));
let slot = match (*st.turn_side_live.lock().unwrap(), main) {
(false, _) => -1,
(true, true) => 0,
(true, false) => 1,
};
resp(200, serde_json::json!({ "slot": slot }).to_string())
}
"/internal/v1/oai/served" => {
let body = req.into_body().collect().await.unwrap().to_bytes();
let v: serde_json::Value = serde_json::from_slice(&body).unwrap();
+96 -5
View File
@@ -680,7 +680,7 @@ async fn tour_complet_par_le_go() {
cfg: cfg.clone(),
patterns: shared.patterns.clone(),
port: 8080,
perf: None,
go: None,
},
compact: TurnCompact {
cfg: CompactConfig::default(),
@@ -693,6 +693,7 @@ async fn tour_complet_par_le_go() {
},
queued: Box::new(Vec::new),
perf_kind: "main",
side_slot: Default::default(),
};
let bash = Tool {
kind: "function".into(),
@@ -1056,7 +1057,7 @@ async fn hote_et_sonde_de_gabarit() {
cfg,
patterns: shared.patterns.clone(),
port: 1,
perf: None,
go: None,
},
compact: TurnCompact {
cfg: CompactConfig::default(),
@@ -1069,6 +1070,7 @@ async fn hote_et_sonde_de_gabarit() {
},
queued: Box::new(Vec::new),
perf_kind: "main",
side_slot: Default::default(),
}
};
let bash = Tool {
@@ -1179,6 +1181,7 @@ async fn contrat_verifie_par_la_facade() {
.await;
let (svc, go, _stop, _t) = service(Some(&url)).await;
*go.code.lock().unwrap() = true;
*go.turn_side_live.lock().unwrap() = true;
let id = svc.sessions.conv().id();
go.set(
"chat",
@@ -1193,6 +1196,39 @@ async fn contrat_verifie_par_la_facade() {
let seen = seen.lock().unwrap().clone();
assert_eq!(seen.len(), 3);
// SIDE_SLOT (lot 31) : le tour sur le slot 0 (rechargement SLOT_PERSIST
// demandé pour la discussion), la vérification sur le 1, comptée en vol
// sur le second slot ; la passe est un travail annexe pour le Go.
let slots: Vec<_> = seen.iter().map(|(_, b)| b["id_slot"].clone()).collect();
assert_eq!(
slots,
[
serde_json::json!(0),
serde_json::json!(1),
serde_json::json!(1)
]
);
assert_eq!(
*go.turn_slots.lock().unwrap(),
[format!("{id} true"), " false".into(), " false".into()]
);
let begins: Vec<String> = go
.oai_log
.lock()
.unwrap()
.iter()
.filter(|l| l.starts_with("begin"))
.cloned()
.collect();
assert_eq!(
begins,
[
format!("begin main_conv={id}"),
"begin main_conv= side".into(),
"begin main_conv= side".into()
]
);
assert_eq!(*go.side_jobs.lock().unwrap(), ["begin 1", "end 1 false"]);
let v = &seen[1].1;
assert_eq!(v["messages"][0]["content"], "VÉRIFICATEUR");
assert_eq!(
@@ -1855,7 +1891,7 @@ async fn parite_du_tour_complet() {
async fn telemetrie_des_resumes() {
use loki_rs::chat::compact::{Summarizer, TranscriptRequest};
let (st, go, _stop, _t) = store().await;
let (url, _seen) = fake_engine(vec![
let (url, seen) = fake_engine(vec![
serde_json::json!({
"choices": [{"message": {"content": "résumé"}, "finish_reason": "stop"}],
"usage": {"prompt_tokens": 3000, "completion_tokens": 200},
@@ -1864,6 +1900,10 @@ async fn telemetrie_des_resumes() {
.to_string(),
r#"{"choices":[]}"#.into(),
"pas du json".into(),
serde_json::json!({
"choices": [{"message": {"content": "suite"}, "finish_reason": "stop"}]
})
.to_string(),
])
.await;
let s = EngineSummarizer {
@@ -1877,8 +1917,9 @@ async fn telemetrie_des_resumes() {
cfg: Default::default(),
patterns: RetryPatterns::builtin(),
port: 8080,
perf: Some((st, "c9".into())),
go: Some((st, "c9".into())),
};
*go.turn_side_live.lock().unwrap() = true;
let req = || TranscriptRequest {
system: "s".into(),
transcript: "t".into(),
@@ -1890,8 +1931,58 @@ async fn telemetrie_des_resumes() {
);
assert!(s.summarize_transcript(req()).await.is_err(), "réponse vide");
assert!(s.summarize_transcript(req()).await.is_err(), "illisible");
// Lot 31 : la transcription, prompt neuf, sur le second slot, comptée en
// vol sur lui et rendue à la fin ; jamais de rechargement SLOT_PERSIST.
let sent = seen.lock().unwrap().clone();
assert!(sent.iter().all(|(_, b)| b["id_slot"] == 1), "{sent:?}");
assert_eq!(
*go.turn_slots.lock().unwrap(),
[" false", " false", " false"]
);
until(|| {
go.oai_log
.lock()
.unwrap()
.iter()
.filter(|l| l.starts_with("end"))
.count()
== 3
})
.await;
assert_eq!(
go.oai_log
.lock()
.unwrap()
.iter()
.filter(|l| *l == "begin main_conv= side")
.count(),
3
);
// En continuation, il prolonge le prompt de la discussion : le slot 0.
use loki_rs::chat::compact::ContinuationRequest;
let cont = s
.summarize_continuation(ContinuationRequest {
messages: vec![],
tools: vec![],
effort: String::new(),
kwargs: Default::default(),
max_tokens: 500,
conv: "c9".into(),
})
.await
.unwrap();
assert_eq!(cont.content, "suite");
assert_eq!(seen.lock().unwrap()[3].1["id_slot"], 0);
assert_eq!(go.turn_slots.lock().unwrap()[3], " true");
until(|| {
go.oai_log
.lock()
.unwrap()
.contains(&"begin main_conv=".to_string())
})
.await;
let perfs = go.perf_records.lock().unwrap().clone();
assert_eq!(perfs.len(), 2, "la réponse illisible n'est pas rangée");
assert_eq!(perfs.len(), 3, "la réponse illisible n'est pas rangée");
assert_eq!(
perfs[0],
serde_json::json!({ "kind": "compact", "conv": "c9", "iter": 0, "complete": true,