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

+59 -1
View File
@@ -37,7 +37,7 @@ use super::compact::{
parse_summary_response,
};
use super::engine::{EngineHttp, classify, friendly_error};
use super::persist::{Persister, StoreSink, ToolResJob};
use super::persist::{Persister, ProjSnapshot, StoreSink, ToolResJob};
use super::stream::{CacheHintFn, PreflightFn, RetryPatterns};
use super::turn::{CompactPhase, PerfMarks, PerfRec, Reply, SendError, ToolOutcome, TurnHost};
use super::types::{AskEvent, DiffLine, Message, Tool};
@@ -366,6 +366,30 @@ pub struct GoTurnHost {
/// La requête qui part va sur le second slot (posé par `engine_slot`,
/// lu par `send` pour le ticket « en vol »).
pub side_slot: AtomicBool,
/// PROJ_SNAPSHOT : la clé et le système du tour (`None` sans la clé), et
/// l'instantané posé par un rafraîchissement en cours de tour.
pub projsnap: Option<SnapTurn>,
pub snap_refreshed: Mutex<Option<ProjSnapshot>>,
}
/// PROJ_SNAPSHOT (lot 34) : ce qu'un tour avec la clé garde pour rafraîchir
/// le bloc projet quand une compaction réécrit l'historique en plein tour
/// (`projSnapTurn`).
#[derive(Debug, Clone)]
pub struct SnapTurn {
pub caps: Value,
pub key: String,
pub sys_hash: String,
}
impl GoTurnHost {
/// L'instantané posé en cours de tour (`takeRefreshed`), s'il y en a un.
pub fn take_refreshed_snap(&self) -> Option<ProjSnapshot> {
self.snap_refreshed
.lock()
.unwrap_or_else(|e| e.into_inner())
.take()
}
}
/// Délai d'une demande de slot : le rechargement SLOT_PERSIST relit jusqu'à
@@ -738,6 +762,40 @@ impl TurnHost for GoTurnHost {
self.shared.echo_refusals.refuse(model);
}
async fn project_refresh(&self, msgs: Vec<Message>) -> Vec<Message> {
let Some(t) = &self.projsnap else {
return msgs;
};
let got = self
.shared
.store
.internal_post(
"/internal/v1/turn/projsnap/refresh",
&json!({ "caps": t.caps, "key": t.key, "sys_hash": t.sys_hash }),
)
.await;
let (block, snap) = match got {
Ok((code, v)) if code.is_success() => (
serde_json::from_value::<Vec<Message>>(v["block"].clone()).ok(),
serde_json::from_value::<ProjSnapshot>(v["snap"].clone()).ok(),
),
_ => (None, None),
};
// Go injoignable : le bloc figé reste, sans les mises à jour (le début
// du prompt change de toute façon) ; l'instantané n'est pas remplacé.
let msgs = super::compact::strip_context_updates(msgs);
match (block, snap) {
(Some(block), Some(snap)) => {
*self
.snap_refreshed
.lock()
.unwrap_or_else(|e| e.into_inner()) = Some(snap);
super::compact::replace_project_head(msgs, &block)
}
_ => msgs,
}
}
fn prewarm_keeps(&self, prefix: &[u64], payload: &Value) -> bool {
self.perf_kind == super::perf::KIND_MAIN
&& self.shared.prewarm.live()