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:
@ -2619,6 +2619,9 @@ pub async fn agent_send(
|
||||
.map(|attachment| attachment.filename.clone())
|
||||
.collect();
|
||||
let session_meta = state.structured_sessions.meta_for_session(&sid);
|
||||
if let Some((project_id, agent_id, _, _)) = &session_meta {
|
||||
state.chat_bridge.bind_owner(sid, *project_id, *agent_id);
|
||||
}
|
||||
let turn_guard = session_meta.as_ref().map(|(project_id, agent_id, _, _)| {
|
||||
backend::StructuredHumanTurnGuard::start_with_source(
|
||||
std::sync::Arc::clone(&state.turn_watch_input),
|
||||
@ -2830,6 +2833,11 @@ pub async fn agent_send(
|
||||
);
|
||||
break;
|
||||
};
|
||||
// The provider replacement owns a fresh SessionId, but this remains
|
||||
// one logical human chat. Move the transport transcript/sink before
|
||||
// opening the continuation stream; writes still made with `sid` are
|
||||
// resolved by ChatBridge to the replacement id.
|
||||
bridge.rebind_session(sid, replacement.id());
|
||||
let resume_prompt = resume_prompt
|
||||
.unwrap_or_else(|| application::SELF_RESTART_CONTINUATION_PROMPT.to_owned());
|
||||
match runtime.block_on(replacement.send(&resume_prompt)) {
|
||||
@ -3413,23 +3421,63 @@ pub async fn set_front_attached(
|
||||
/// then receives subsequent chunks over `on_reply`.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an [`ErrorDto`] (`INVALID` for a malformed id, `NOT_FOUND` if no live
|
||||
/// structured session owns the id — the caller then falls back to a fresh launch).
|
||||
/// `project_id` + `agent_id` are an optional stable fallback for callers whose
|
||||
/// stored transport id predates a restart. The response always carries the
|
||||
/// effective live `sessionId`, its current scrollback and its provider
|
||||
/// `conversationId` when known.
|
||||
///
|
||||
/// Returns an [`ErrorDto`] (`INVALID` for malformed ids, `NOT_FOUND` if neither
|
||||
/// the requested/successor session nor the project-agent fallback is live).
|
||||
#[tauri::command]
|
||||
pub fn reattach_agent_chat(
|
||||
session_id: String,
|
||||
project_id: Option<String>,
|
||||
agent_id: Option<String>,
|
||||
on_reply: Channel<ReplyChunk>,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<ReattachChatDto, ErrorDto> {
|
||||
let sid = parse_session_id(&session_id)?;
|
||||
let requested_sid = parse_session_id(&session_id)?;
|
||||
|
||||
// A missing live session means the conversation is gone (closed/never live) —
|
||||
// surfaced as NOT_FOUND so the caller falls back to opening a fresh cell.
|
||||
// A restart replaces the provider session id. Prefer the bridge successor
|
||||
// recorded by the restart-aware pump, then allow the caller to recover by
|
||||
// stable project/agent identity if it only retained a dead id.
|
||||
let mut sid = state.chat_bridge.successor(&requested_sid);
|
||||
if state.structured_sessions.session(&sid).is_none() {
|
||||
return Err(ErrorDto::from(AppError::NotFound(format!(
|
||||
"structured session {sid}"
|
||||
))));
|
||||
let explicit_owner = match (project_id.as_deref(), agent_id.as_deref()) {
|
||||
(Some(project_id), Some(agent_id)) => {
|
||||
Some((parse_project_id(project_id)?, parse_agent_id(agent_id)?))
|
||||
}
|
||||
(None, None) => None,
|
||||
_ => {
|
||||
return Err(ErrorDto::from(AppError::Invalid(
|
||||
"projectId and agentId must be provided together".to_owned(),
|
||||
)));
|
||||
}
|
||||
};
|
||||
sid = match explicit_owner.or_else(|| state.chat_bridge.owner(&requested_sid)) {
|
||||
Some((project_id, agent_id)) => {
|
||||
let replacement = state
|
||||
.structured_sessions
|
||||
.session_id_for_agent_in_project(project_id, &agent_id)
|
||||
.ok_or_else(|| {
|
||||
ErrorDto::from(AppError::NotFound(format!(
|
||||
"live structured session for agent {agent_id}"
|
||||
)))
|
||||
})?;
|
||||
state.chat_bridge.rebind_session(requested_sid, replacement);
|
||||
replacement
|
||||
}
|
||||
None => {
|
||||
return Err(ErrorDto::from(AppError::NotFound(format!(
|
||||
"structured session {requested_sid}"
|
||||
))));
|
||||
}
|
||||
};
|
||||
}
|
||||
let conversation_id = state
|
||||
.structured_sessions
|
||||
.meta_for_session(&sid)
|
||||
.and_then(|(_, _, _, conversation_id)| conversation_id);
|
||||
|
||||
// (1) Snapshot the conversation scrollback before swapping the channel.
|
||||
let scrollback = state.chat_bridge.scrollback(&sid);
|
||||
@ -3440,7 +3488,8 @@ pub fn reattach_agent_chat(
|
||||
let _gen = state.chat_bridge.register(sid, on_reply);
|
||||
|
||||
Ok(ReattachChatDto {
|
||||
session_id,
|
||||
session_id: sid.to_string(),
|
||||
conversation_id,
|
||||
scrollback,
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user