mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-07 17:09:13 -10:00
fix(tui): checkpoint the visible partial reply during a running turn [skip ci]
CW-7553 and #7095. The recovery interval wrote only fully committed API messages, so a SIGKILL after the checkpoint advanced still lost the visible answer. For the active turn with the matching runtime ID and a user-message tail, append streaming assistant prose to the checkpoint journal copy using the existing interrupted-assistant role. The live journal stays with the Engine. Its normal completion replaces the checkpoint. Suppressed stream events, stale turns and completed messages cannot produce an extra tail. On real Ubuntu ARM64 the baseline lost the visible reply immediately and after a 43.44-second checkpoint advance. The candidate recovered it after the checkpoint, resumed without a provider call and sent exactly one marked copy on explicit follow-up. A normally finished stream reopened as one completed message. SIGTERM retained the partial, /copy retained the completed answer, and the immediate SIGKILL still had only previously durable content. Four recovery journeys and five adjacent Auto-window journeys passed on afb29f01be10. One code block changed only by rustfmt after the binary freeze. Original Deepin and native Windows are separate qualifications.
This commit is contained in:
1 parent
de99f0530a
commit
5fd52e87c8
3 files changed
+37
-14
No files matched your search
@@ -4918,19 +4918,11 @@ impl Engine {
|
||||
if text.is_empty() {
|
||||
return;
|
||||
}
|
||||
let message = Message {
|
||||
role: Role::InterruptedAssistant,
|
||||
content: vec![ContentBlock::Text {
|
||||
text: text.to_string(),
|
||||
cache_control: None,
|
||||
}],
|
||||
};
|
||||
let already_committed = self.session.messages.last().is_some_and(|last| {
|
||||
matches!(
|
||||
last.role.as_str(),
|
||||
"assistant" | codewhale_models::INTERRUPTED_ASSISTANT_ROLE
|
||||
) && last.content == message.content
|
||||
});
|
||||
let message = Message::interrupted_assistant(text);
|
||||
let already_committed =
|
||||
self.session.messages.last().is_some_and(|last| {
|
||||
last.role.is_assistant_like() && last.content == message.content
|
||||
});
|
||||
if already_committed {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -114,6 +114,18 @@ pub struct Message {
|
||||
pub content: Vec<ContentBlock>,
|
||||
}
|
||||
|
||||
impl Message {
|
||||
pub fn interrupted_assistant(text: &str) -> Self {
|
||||
Self {
|
||||
role: Role::InterruptedAssistant,
|
||||
content: vec![ContentBlock::Text {
|
||||
text: text.to_owned(),
|
||||
cache_control: None,
|
||||
}],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Internal role used for assistant text that was visible before a turn was interrupted.
|
||||
pub const INTERRUPTED_ASSISTANT_ROLE: &str = "assistant_interrupted";
|
||||
/// Prefix attached to interrupted assistant output when it is replayed as context.
|
||||
|
||||
@@ -460,8 +460,27 @@ pub(crate) fn reconcile_turn_liveness_with(
|
||||
/// losing the entire in-progress turn.
|
||||
pub(crate) fn persist_recovery_snapshot(app: &mut App) {
|
||||
if let Ok(manager) = SessionManager::default_location()
|
||||
&& let Ok(session) = build_session_snapshot(app, &manager)
|
||||
&& let Ok(mut session) = build_session_snapshot(app, &manager)
|
||||
{
|
||||
if let Some(turn) = app.active_turn.as_ref()
|
||||
&& app.runtime_turn_id.as_deref() == Some(turn.turn_id.as_str())
|
||||
&& !app.suppress_stream_events_until_turn_complete
|
||||
&& app
|
||||
.api_messages
|
||||
.last()
|
||||
.is_some_and(|message| message.role == codewhale_models::Role::User)
|
||||
&& let Some(HistoryCell::Assistant {
|
||||
content,
|
||||
streaming: true,
|
||||
}) = app
|
||||
.streaming_message_index
|
||||
.and_then(|index| app.history.get(index))
|
||||
&& !content.is_empty()
|
||||
&& let Some(journal) = session.journal.as_mut()
|
||||
{
|
||||
session.leaf_id = Some(journal.append_message(Message::interrupted_assistant(content)));
|
||||
session.metadata.message_count += 1;
|
||||
}
|
||||
let projection = saved_work_projection(app, &session);
|
||||
if app.current_session_id.is_none() {
|
||||
app.current_session_id = Some(session.metadata.id.clone());
|
||||
|
||||
Reference in new issue
Block a user