mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-07 17:09:13 -10:00
fix(tui): announce restored queued messages on resume and log queue write errors (CW-7427)
#6978: on every launch, a Windows 0.10.0 user saw the same three old messages
re-sent as turns 2-4. A source review of v0.10.0 and main traced them to the
session's parked offline queue, checkpoints/<session>.offline_queue.json. The
review used a five-agent workflow: one investigator per hypothesis, plus an
independent judge.
Each launch restores that file, then sends one message after each finished
turn. As the queue drains, the app does request "save 2", "save 1" and then
a delete, and nothing can skip those requests. So a file stuck at three
messages means writes to that one file fail on the reporter's machine. Four
other causes were each ruled out: session ids, crash checkpoints, the
snapshot store, and the drain logic.
Two gaps made this invisible:
- The "Restored N queued message(s)" notice appeared only when no other
status was set. A resume always sets "Resumed session: ...", so a resumed
session never said that old messages were about to be sent. The notice
also named Ctrl+X, which does not discard the queue. It is now a warning
toast with a typed, localized message (QueueRestoredFromPreviousSession,
in all 15 complete packs). It says the messages send after the next
message and names /queue clear. The resume receipt is unchanged.
- A failed queue write was logged only as an error category, without the
session or the OS error, so error 32 (file in use) read as
"Uncategorized". Every failed persistence write is now logged where it
happens, with its full error including the OS code. The queue labels carry
the session id (offline-queue:<id>, clear-offline-queue:<id>). The
separate category-only log pass is removed, so each failure logs once.
The startup re-park of a restored queue is removed. Its comment claimed the
queue's clear request carries no session id, but it does. The restored file
already holds exactly what was restored, so the re-park rewrote identical
content.
Not changed: messages restored from a previous launch still send
automatically after the next message. Holding them until the user sends
them explicitly is a product decision left to the founder. The on-disk cause
on the reporter's machine is not reproduced. The reply on #6978 asks for the
file timestamps, ACL and handle state, and the now-informative log lines.
Verification on one combined tree: b457a8affd plus this commit and the G1
MCP handoff commit, which touch disjoint files.
- cargo fmt --all -- --check: clean.
- clippy -p codewhale-tui -p codewhale-localization --all-targets --locked
with CI's flags: clean.
- nextest, focused on offline queue, persistence, queue, localization,
toasts, MCP, session boundary, sync session and supervisor, plus the
whole codewhale-localization package: 978 run, 977 passed, 1 failed.
The failure is runtime_api::tests::mcp_management_selected_connection_and_retry_preserve_siblings
("MCP configuration changed; reload it before connecting"). It comes from
a runtime API config-hash comparison neither commit touches, and it
passed 5 of 5 isolated reruns. That run also included the G1 commit's
scratch probe, since removed; the test file is identical to HEAD.
- npm test: 547 Node tests (540 passed, 7 skipped, 0 failed) and
772 Vitest passed. npm run check:web: pass.
- Not run: a Windows build or the reporter's environment.
Refs CW-7427, #6978
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NBKbhx19BRbZtvWK8rVKjz
This commit is contained in:
1 parent
b457a8affd
commit
b6ce70b12e
18 files changed
+35
-31
No files matched your search
@@ -646,6 +646,7 @@
|
||||
"CmdLoginDescription": "Inicia sessió a Codewhale o desa claus de proveïdor",
|
||||
"CmdQueueDescription": "Mostra o edita els missatges en cua",
|
||||
"CmdQueueUsage": "Ús: /queue [list|send <n>|edit <n>|drop <n>|clear]",
|
||||
"QueueRestoredFromPreviousSession": "S'han restaurat {count} missatges en cua; s'enviaran després del teu proper missatge. /queue clear els descarta.",
|
||||
"CmdQueueDraftHeader": "S'està editant el missatge en cua:",
|
||||
"CmdQueueNoMessages": "No hi ha missatges en cua",
|
||||
"CmdQueueListHeader": "Missatges en cua ({count}):",
|
||||
|
||||
@@ -646,6 +646,7 @@
|
||||
"CmdLoginDescription": "Bei Codewhale anmelden oder Provider-Schlüssel speichern",
|
||||
"CmdQueueDescription": "Nachrichten in der Warteschlange ansehen oder bearbeiten",
|
||||
"CmdQueueUsage": "Verwendung: /queue [list|send <n>|edit <n>|drop <n>|clear]",
|
||||
"QueueRestoredFromPreviousSession": "{count} Nachricht(en) aus der Warteschlange wiederhergestellt; sie werden nach deiner nächsten Nachricht gesendet. /queue clear verwirft sie.",
|
||||
"CmdQueueDraftHeader": "Nachricht aus der Warteschlange bearbeiten:",
|
||||
"CmdQueueNoMessages": "Keine Nachrichten in der Warteschlange",
|
||||
"CmdQueueListHeader": "Nachrichten in der Warteschlange ({count}):",
|
||||
|
||||
@@ -649,6 +649,7 @@
|
||||
"CmdLoginDescription": "Sign in to Codewhale or store provider keys",
|
||||
"CmdQueueDescription": "See or edit queued messages",
|
||||
"CmdQueueUsage": "Usage: /queue [list|send <n>|edit <n>|drop <n>|clear]",
|
||||
"QueueRestoredFromPreviousSession": "Restored {count} queued message(s); they send after your next message. /queue clear discards them.",
|
||||
"CmdQueueDraftHeader": "Editing queued message:",
|
||||
"CmdQueueNoMessages": "No queued messages",
|
||||
"CmdQueueListHeader": "Queued messages ({count}):",
|
||||
|
||||
@@ -649,6 +649,7 @@
|
||||
"CmdLoginDescription": "Iniciar sesión en Codewhale o guardar claves de proveedor",
|
||||
"CmdQueueDescription": "Ver o editar mensajes en cola",
|
||||
"CmdQueueUsage": "Uso: /queue [list|send <n>|edit <n>|drop <n>|clear]",
|
||||
"QueueRestoredFromPreviousSession": "Se restauraron {count} mensajes en cola; se enviarán después de tu próximo mensaje. /queue clear los descarta.",
|
||||
"CmdQueueDraftHeader": "Editando mensaje en cola:",
|
||||
"CmdQueueNoMessages": "No hay mensajes en cola",
|
||||
"CmdQueueListHeader": "Mensajes en cola ({count}):",
|
||||
|
||||
@@ -646,6 +646,7 @@
|
||||
"CmdLoginDescription": "Se connecter à Codewhale ou enregistrer des clés fournisseur",
|
||||
"CmdQueueDescription": "Voir ou modifier les messages en file",
|
||||
"CmdQueueUsage": "Usage : /queue [list|send <n>|edit <n>|drop <n>|clear]",
|
||||
"QueueRestoredFromPreviousSession": "{count} message(s) en file restauré(s) ; ils partiront après votre prochain message. /queue clear les supprime.",
|
||||
"CmdQueueDraftHeader": "Modification du message en file :",
|
||||
"CmdQueueNoMessages": "Aucun message en file",
|
||||
"CmdQueueListHeader": "Messages en file ({count}) :",
|
||||
|
||||
@@ -646,6 +646,7 @@
|
||||
"CmdLoginDescription": "Codewhale में साइन इन करें या प्रदाता कुंजी सहेजें",
|
||||
"CmdQueueDescription": "कतारबद्ध संदेश देखें या संपादित करें",
|
||||
"CmdQueueUsage": "उपयोग: /queue [list|send <n>|edit <n>|drop <n>|clear]",
|
||||
"QueueRestoredFromPreviousSession": "कतार के {count} संदेश बहाल किए गए; ये आपके अगले संदेश के बाद भेजे जाएंगे। इन्हें हटाने के लिए /queue clear।",
|
||||
"CmdQueueDraftHeader": "कतारबद्ध संदेश संपादित हो रहा है:",
|
||||
"CmdQueueNoMessages": "कोई कतारबद्ध संदेश नहीं",
|
||||
"CmdQueueListHeader": "कतारबद्ध संदेश ({count}):",
|
||||
|
||||
@@ -646,6 +646,7 @@
|
||||
"CmdLoginDescription": "Masuk ke Codewhale atau simpan kunci penyedia",
|
||||
"CmdQueueDescription": "Lihat atau edit pesan dalam antrean",
|
||||
"CmdQueueUsage": "Penggunaan: /queue [list|send <n>|edit <n>|drop <n>|clear]",
|
||||
"QueueRestoredFromPreviousSession": "{count} pesan dalam antrean dipulihkan; pesan akan dikirim setelah pesan Anda berikutnya. /queue clear untuk membuangnya.",
|
||||
"CmdQueueDraftHeader": "Mengedit pesan dalam antrean:",
|
||||
"CmdQueueNoMessages": "Tidak ada pesan dalam antrean",
|
||||
"CmdQueueListHeader": "Pesan dalam antrean ({count}):",
|
||||
|
||||
@@ -649,6 +649,7 @@
|
||||
"CmdLoginDescription": "Codewhaleにサインインするか、プロバイダーキーを保存",
|
||||
"CmdQueueDescription": "キューされたメッセージを確認・編集",
|
||||
"CmdQueueUsage": "使用方法: /queue [list|send <n>|edit <n>|drop <n>|clear]",
|
||||
"QueueRestoredFromPreviousSession": "キューのメッセージ {count} 件を復元しました。次のメッセージの後に送信されます。破棄するには /queue clear。",
|
||||
"CmdQueueDraftHeader": "キューされたメッセージを編集中:",
|
||||
"CmdQueueNoMessages": "キューされたメッセージはありません",
|
||||
"CmdQueueListHeader": "キューされたメッセージ ({count}):",
|
||||
|
||||
@@ -649,6 +649,7 @@
|
||||
"CmdLoginDescription": "Codewhale에 로그인하거나 프로바이더 키를 저장합니다",
|
||||
"CmdQueueDescription": "대기 중인 메시지를 확인하거나 편집합니다",
|
||||
"CmdQueueUsage": "사용법: /queue [list|send <n>|edit <n>|drop <n>|clear]",
|
||||
"QueueRestoredFromPreviousSession": "대기 중인 메시지 {count}개를 복원했습니다. 다음 메시지 후에 전송됩니다. /queue clear로 삭제할 수 있습니다.",
|
||||
"CmdQueueDraftHeader": "대기 중인 메시지 편집 중:",
|
||||
"CmdQueueNoMessages": "대기 중인 메시지가 없습니다",
|
||||
"CmdQueueListHeader": "대기 중인 메시지 ({count}개):",
|
||||
|
||||
@@ -649,6 +649,7 @@
|
||||
"CmdLoginDescription": "Entrar no Codewhale ou salvar chaves de provedor",
|
||||
"CmdQueueDescription": "Ver ou editar mensagens enfileiradas",
|
||||
"CmdQueueUsage": "Uso: /queue [list|send <n>|edit <n>|drop <n>|clear]",
|
||||
"QueueRestoredFromPreviousSession": "{count} mensagem(ns) enfileirada(s) restaurada(s); serão enviadas após sua próxima mensagem. /queue clear as descarta.",
|
||||
"CmdQueueDraftHeader": "Editando mensagem enfileirada:",
|
||||
"CmdQueueNoMessages": "Nenhuma mensagem enfileirada",
|
||||
"CmdQueueListHeader": "Mensagens enfileiradas ({count}):",
|
||||
|
||||
@@ -646,6 +646,7 @@
|
||||
"CmdLoginDescription": "Войти в Codewhale или сохранить ключи провайдера",
|
||||
"CmdQueueDescription": "Просмотр и редактирование сообщений в очереди",
|
||||
"CmdQueueUsage": "Использование: /queue [list|send <n>|edit <n>|drop <n>|clear]",
|
||||
"QueueRestoredFromPreviousSession": "Восстановлено сообщений в очереди: {count}; они отправятся после вашего следующего сообщения. /queue clear удалит их.",
|
||||
"CmdQueueDraftHeader": "Редактирование сообщения из очереди:",
|
||||
"CmdQueueNoMessages": "Нет сообщений в очереди",
|
||||
"CmdQueueListHeader": "Сообщения в очереди ({count}):",
|
||||
|
||||
@@ -646,6 +646,7 @@
|
||||
"CmdLoginDescription": "Увійти в Codewhale або зберегти ключі провайдера",
|
||||
"CmdQueueDescription": "Переглянути або редагувати повідомлення в черзі",
|
||||
"CmdQueueUsage": "Використання: /queue [list|send <n>|edit <n>|drop <n>|clear]",
|
||||
"QueueRestoredFromPreviousSession": "Відновлено повідомлень у черзі: {count}; їх буде надіслано після вашого наступного повідомлення. /queue clear видалить їх.",
|
||||
"CmdQueueDraftHeader": "Редагування повідомлення з черги:",
|
||||
"CmdQueueNoMessages": "Немає повідомлень у черзі",
|
||||
"CmdQueueListHeader": "Повідомлення в черзі ({count}):",
|
||||
|
||||
@@ -649,6 +649,7 @@
|
||||
"CmdLoginDescription": "Đăng nhập Codewhale hoặc lưu khóa nhà cung cấp",
|
||||
"CmdQueueDescription": "Xem hoặc chỉnh sửa các tin nhắn đang chờ xử lý",
|
||||
"CmdQueueUsage": "Cách dùng: /queue [list|send <n>|edit <n>|drop <n>|clear]",
|
||||
"QueueRestoredFromPreviousSession": "Đã khôi phục {count} tin nhắn đang chờ; chúng sẽ được gửi sau tin nhắn tiếp theo của bạn. /queue clear để hủy chúng.",
|
||||
"CmdQueueDraftHeader": "Đang chỉnh sửa tin nhắn đang chờ:",
|
||||
"CmdQueueNoMessages": "Không có tin nhắn đang chờ",
|
||||
"CmdQueueListHeader": "Tin nhắn đang chờ ({count}):",
|
||||
|
||||
@@ -649,6 +649,7 @@
|
||||
"CmdLoginDescription": "登录 Codewhale 或保存提供商密钥",
|
||||
"CmdQueueDescription": "查看或编辑已排队的消息",
|
||||
"CmdQueueUsage": "用法: /queue [list|send <n>|edit <n>|drop <n>|clear]",
|
||||
"QueueRestoredFromPreviousSession": "已恢复 {count} 条排队消息;它们会在你发送下一条消息后发出。/queue clear 可将其丢弃。",
|
||||
"CmdQueueDraftHeader": "正在编辑已排队的消息:",
|
||||
"CmdQueueNoMessages": "没有已排队的消息",
|
||||
"CmdQueueListHeader": "已排队的消息 ({count}):",
|
||||
|
||||
@@ -647,6 +647,7 @@
|
||||
"CmdLoginDescription": "登入 Codewhale 或儲存供應商金鑰",
|
||||
"CmdQueueDescription": "檢視或編輯已排隊的訊息",
|
||||
"CmdQueueUsage": "用法: /queue [list|send <n>|edit <n>|drop <n>|clear]",
|
||||
"QueueRestoredFromPreviousSession": "已還原 {count} 則排隊訊息;它們會在你送出下一則訊息後發出。/queue clear 可將其捨棄。",
|
||||
"CmdQueueDraftHeader": "正在編輯已排隊的訊息:",
|
||||
"CmdQueueNoMessages": "沒有已排隊的訊息",
|
||||
"CmdQueueListHeader": "已排隊的訊息 ({count}):",
|
||||
|
||||
@@ -791,6 +791,7 @@ pub enum MessageId {
|
||||
CmdProviderDescription,
|
||||
CmdQueueDescription,
|
||||
CmdQueueUsage,
|
||||
QueueRestoredFromPreviousSession,
|
||||
CmdQueueDraftHeader,
|
||||
CmdQueueNoMessages,
|
||||
CmdQueueListHeader,
|
||||
@@ -3457,6 +3458,7 @@ pub const ALL_MESSAGE_IDS: &[MessageId] = &[
|
||||
MessageId::CmdProviderDescription,
|
||||
MessageId::CmdQueueDescription,
|
||||
MessageId::CmdQueueUsage,
|
||||
MessageId::QueueRestoredFromPreviousSession,
|
||||
MessageId::CmdQueueDraftHeader,
|
||||
MessageId::CmdQueueNoMessages,
|
||||
MessageId::CmdQueueListHeader,
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
//! Checkpoints and clears are keyed by session id, so concurrent sessions
|
||||
//! never coalesce into (or clear) each other's slot.
|
||||
//! - **Durability reporting**: `FlushAndReport` returns accumulated results;
|
||||
//! cycles without a listener log failures instead of discarding them.
|
||||
//! every failed write is also logged with its OS error when it happens.
|
||||
//! - **Bounded command channel with sender-side coalescing** (#6212):
|
||||
//! `try_send` absorbs each request into a shared latest-wins state at send
|
||||
//! time and wakes the actor through a small bounded channel, so a paused
|
||||
@@ -412,7 +412,6 @@ pub fn spawn_persistence_actor(
|
||||
health: &SessionSaveHealth,
|
||||
) {
|
||||
let cycle = flush_inner(manager, pending, health);
|
||||
log_flush_failures(&cycle);
|
||||
unreported.merge(cycle);
|
||||
}
|
||||
|
||||
@@ -589,7 +588,16 @@ fn flush_inner(
|
||||
let mut report = FlushReport::default();
|
||||
let mut record = |what: String, result: std::io::Result<()>| match result {
|
||||
Ok(()) => report.completed += 1,
|
||||
Err(err) => report.failures.push((what, err.kind())),
|
||||
Err(err) => {
|
||||
tracing::warn!(
|
||||
target: "persistence",
|
||||
what = %what,
|
||||
error_kind = ?err.kind(),
|
||||
error = %err,
|
||||
"persistence write failed",
|
||||
);
|
||||
report.failures.push((what, err.kind()));
|
||||
}
|
||||
};
|
||||
|
||||
// Document saves are bounded (#6842): an oversized journal is archived
|
||||
@@ -632,13 +640,13 @@ fn flush_inner(
|
||||
for (_, request) in std::mem::take(&mut pending.offline_queue) {
|
||||
match request {
|
||||
PendingOfflineQueue::Save { state, lease } => record(
|
||||
"offline-queue".to_string(),
|
||||
format!("offline-queue:{}", lease.session_id()),
|
||||
manager
|
||||
.save_offline_queue_state(&state, Some(lease.session_id()))
|
||||
.map(|_| ()),
|
||||
),
|
||||
PendingOfflineQueue::Clear { lease } => record(
|
||||
"clear-offline-queue".to_string(),
|
||||
format!("clear-offline-queue:{}", lease.session_id()),
|
||||
manager.clear_offline_queue_state_for(lease.session_id()),
|
||||
),
|
||||
}
|
||||
@@ -701,19 +709,6 @@ impl SessionSaveHealth {
|
||||
}
|
||||
}
|
||||
|
||||
/// Surface flush failures in the log for write cycles that have no caller
|
||||
/// waiting on a [`FlushReport`].
|
||||
fn log_flush_failures(report: &FlushReport) {
|
||||
for (what, kind) in &report.failures {
|
||||
tracing::warn!(
|
||||
target: "persistence",
|
||||
what = %what,
|
||||
error_kind = ?kind,
|
||||
"persistence write failed",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "persistence_actor/tests.rs"]
|
||||
mod backlog_measurement_tests;
|
||||
|
||||
@@ -1051,11 +1051,11 @@ pub async fn run_tui(
|
||||
let restored_offline_queue = install_offline_queue_transition(&mut app, transition)
|
||||
|| !app.queued_messages.is_empty()
|
||||
|| app.queued_draft.is_some();
|
||||
if restored_offline_queue && app.status_message.is_none() && app.queued_message_count() > 0 {
|
||||
app.status_message = Some(format!(
|
||||
"Restored {} queued message(s) from previous session — ↑ to edit, Ctrl+X to discard",
|
||||
app.queued_message_count()
|
||||
));
|
||||
if restored_offline_queue && app.queued_message_count() > 0 {
|
||||
let restored = app
|
||||
.tr(MessageId::QueueRestoredFromPreviousSession)
|
||||
.replace("{count}", &app.queued_message_count().to_string());
|
||||
app.push_status_toast(restored, StatusToastLevel::Warning, None);
|
||||
}
|
||||
|
||||
let task_manager = TaskManager::start(
|
||||
@@ -1208,14 +1208,6 @@ pub async fn run_tui(
|
||||
(handle, task)
|
||||
});
|
||||
|
||||
// Re-park the queue restored above, now that the actor exists. Its clear
|
||||
// request carries no session id, so the actor learns which session owns
|
||||
// the parked file from a save — without this, draining a restored queue
|
||||
// to empty would leave the file behind and resend it on the next boot.
|
||||
if restored_offline_queue {
|
||||
persist_offline_queue_state(&app);
|
||||
}
|
||||
|
||||
// #4605: create the dispatch completion channel before any submit path so
|
||||
// initial input and queued follow-ups can dispatch without blocking the
|
||||
// startup sequence.
|
||||
|
||||
Reference in new issue
Block a user