fix(chat): le transcript s'affiche à nouveau après redémarrage automatique d'un agent — #295 (app-tauri ChatBridge: cartes successors/owners, rebind_session déplace sink+scrolloback du logical chat vers la session de remplacement et canonical_session résout les écritures avec l'ancien id sur toutes les opérations register/scrollback/detach/send_output; backend stream: ReplayOutputBridge::rekey prépare le scrollback source devant le destination et invalide les générations pré-rekey des deux pumps; commands: agent_send binde l'owner stable (projectId,agentId) au tour humain et rebind avant le flux de continuation au handoff restart; reattach_agent_chat résout le successeur du bridge puis fallback identité stable projectId/agentId explicite ou owner bindé, renvoie sessionId effectif + conversationId, NOT_FOUND sinon; web-server chat.attach même fallback stale→live par project/agent; DTO ReattachChatDto/ReattachResultDto documentent l'id effectif post-restart; frontend: ReattachAgentChatOptions, conversationId dans ChatAttachedPayload, CustomAgentChatView réattache sur agentRestartCompleted (dédupe par restartId, scrollback appliqué, tours restart préservés), retry identité stable sur NOT_FOUND dans openOrAttach avant fallback launch frais, publication sessionId effectif + conversationId, état vide «Conversation liée» quand une conversation/run/workstate lié existe; QA verte reconfirmée par Git: git diff --check, cargo fmt --all --check, app-tauri chat_bridge 34/34 dont 3 nouveaux restart_rebind + dto_chat 24/24, cargo check -p app-tauri, backend 91+7 ignored permissions socket, web-server websocket_chat_attach 3/3, npm test 77/77 CustomAgentChatView+agentGateway, npm run build; réserves non bloquantes: warnings Vite esbuild/oxc et chunk >500kB connus)
This commit is contained in:
@ -1284,7 +1284,8 @@ impl From<OpenTerminalOutput> for TerminalSessionDto {
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ReattachResultDto {
|
||||
/// The session that was re-attached (echoed back for the frontend).
|
||||
/// The effective live session that was re-attached. This can differ from the
|
||||
/// requested id after a structured-runtime restart.
|
||||
pub session_id: String,
|
||||
/// The most-recent retained output bytes (scrollback ring buffer).
|
||||
pub scrollback: Vec<u8>,
|
||||
@ -3678,8 +3679,13 @@ pub enum ReplyChunk {
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ReattachChatDto {
|
||||
/// The session that was re-attached (echoed back for the frontend).
|
||||
/// The effective live session that was re-attached. This can differ from the
|
||||
/// requested id after a structured-runtime restart.
|
||||
pub session_id: String,
|
||||
/// Effective provider conversation id of the live replacement session, when
|
||||
/// the adapter exposes one.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub conversation_id: Option<String>,
|
||||
/// The chunks already streamed for this conversation, in order. The frontend
|
||||
/// replays them to rebuild the visible turns, then receives subsequent chunks
|
||||
/// over the freshly-registered channel.
|
||||
|
||||
@ -170,6 +170,41 @@ where
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Moves the retained stream state from `from` to `to`.
|
||||
///
|
||||
/// When `to` already exists, its live sink wins and the source scrollback is
|
||||
/// prepended to the destination scrollback. This is used when a logical chat
|
||||
/// survives a provider restart under a fresh transport session id.
|
||||
pub fn rekey(&self, from: &K, to: K) {
|
||||
if *from == to {
|
||||
return;
|
||||
}
|
||||
if let Ok(mut map) = self.entries.lock() {
|
||||
let Some(source) = map.remove(from) else {
|
||||
return;
|
||||
};
|
||||
match map.get_mut(&to) {
|
||||
Some(destination) => {
|
||||
// Invalidate both pre-rekey pump generations. In particular,
|
||||
// the dying source pump must never detach a sink that was
|
||||
// already attached to the replacement key.
|
||||
let mut generation = destination.generation.wrapping_add(1);
|
||||
if generation == source.generation {
|
||||
generation = generation.wrapping_add(1);
|
||||
}
|
||||
destination.generation = generation;
|
||||
let mut scrollback = source.scrollback;
|
||||
scrollback.append(&mut destination.scrollback);
|
||||
destination.scrollback = scrollback;
|
||||
trim_scrollback(destination, self.max_chunks, self.max_bytes, self.measure);
|
||||
}
|
||||
None => {
|
||||
map.insert(to, source);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unregister(&self, key: &K) {
|
||||
if let Ok(mut map) = self.entries.lock() {
|
||||
map.remove(key);
|
||||
|
||||
Reference in New Issue
Block a user