mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-07 10:36:05 -10:00
Merge current main into the 0.10.2 candidate
Brings in #6985 (Standard Compute), #6989 (OHOS), the CW-6880 mobile approval-card fix, the CW-7427 queue notice and logging fix and the G1 stdio MCP handoff, so the candidate's CI qualifies the tree the squash will produce. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01NBKbhx19BRbZtvWK8rVKjz
This commit is contained in:
commit
ba01538bc9
24 files changed
+161
-52
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}):",
|
||||
|
||||
@@ -792,6 +792,7 @@ pub enum MessageId {
|
||||
CmdProviderDescription,
|
||||
CmdQueueDescription,
|
||||
CmdQueueUsage,
|
||||
QueueRestoredFromPreviousSession,
|
||||
CmdQueueDraftHeader,
|
||||
CmdQueueNoMessages,
|
||||
CmdQueueListHeader,
|
||||
@@ -3459,6 +3460,7 @@ pub const ALL_MESSAGE_IDS: &[MessageId] = &[
|
||||
MessageId::CmdProviderDescription,
|
||||
MessageId::CmdQueueDescription,
|
||||
MessageId::CmdQueueUsage,
|
||||
MessageId::QueueRestoredFromPreviousSession,
|
||||
MessageId::CmdQueueDraftHeader,
|
||||
MessageId::CmdQueueNoMessages,
|
||||
MessageId::CmdQueueListHeader,
|
||||
|
||||
@@ -1004,6 +1004,7 @@ pub struct Engine {
|
||||
/// Abort the supervisor's reconnect handshakes when its pool is retired.
|
||||
mcp_supervisor_task: Option<tokio::task::AbortHandle>,
|
||||
mcp_boot_done: Option<tokio::sync::watch::Receiver<bool>>,
|
||||
mcp_handoff: Option<crate::mcp::McpHandoff>,
|
||||
/// Generation owned by the currently installed boot receiver. Terminal
|
||||
/// cleanup is conditional on this exact value so an older pass can never
|
||||
/// clear a newer receiver.
|
||||
@@ -2337,6 +2338,7 @@ impl Engine {
|
||||
mcp_supervisor_rx: None,
|
||||
mcp_supervisor_task: None,
|
||||
mcp_boot_done: None,
|
||||
mcp_handoff: None,
|
||||
mcp_boot_generation: None,
|
||||
mcp_boot_task: None,
|
||||
mcp_event_generation: 0,
|
||||
@@ -3109,8 +3111,17 @@ impl Engine {
|
||||
// authoritative schedule makes that token fail closed when drained.
|
||||
self.scheduled_goal_continuation = None;
|
||||
// Runtime-added MCP servers are conversation capabilities even when
|
||||
// both conversations use the same workspace. The sync handler restarts
|
||||
// the session boot once the new session is installed.
|
||||
// both conversations use the same workspace, so the old pool retires.
|
||||
// The sync handler starts the session boot once the new session is
|
||||
// installed.
|
||||
self.mcp_handoff = if self.child_host.is_none() {
|
||||
self.mcp_pool
|
||||
.as_ref()
|
||||
.and_then(|pool| pool.try_lock().ok())
|
||||
.map(|mut pool| pool.retire_for_handoff())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
self.drop_mcp_pool();
|
||||
// C02-17: cumulative usage belongs to the conversation that spent
|
||||
// it. The sync carries no prior total for the installed session, so
|
||||
@@ -4037,6 +4048,7 @@ impl Engine {
|
||||
crate::mcp::format_mcp_error_for_display(&error)
|
||||
);
|
||||
}
|
||||
self.mcp_handoff = None;
|
||||
// SessionUpdated acknowledges the sync. A generic status
|
||||
// would immediately cover the host's confirmed resume receipt.
|
||||
self.emit_session_updated().await;
|
||||
@@ -8213,6 +8225,10 @@ impl Engine {
|
||||
/// pool, skipped a session boot). Progress and the finished receipt flow
|
||||
/// as `Event::McpSessionBoot` updates under that generation.
|
||||
async fn start_mcp_session_boot(&mut self, refresh: McpConnectRefresh) -> anyhow::Result<u64> {
|
||||
let handoff = self
|
||||
.mcp_handoff
|
||||
.take()
|
||||
.filter(|_| matches!(refresh, McpConnectRefresh::IfChanged));
|
||||
if matches!(refresh, McpConnectRefresh::IfChanged)
|
||||
&& (self.child_host.is_some() || !self.config.features.enabled(Feature::Mcp))
|
||||
{
|
||||
@@ -8250,6 +8266,9 @@ impl Engine {
|
||||
// so a failed explicit reload leaves the live pool intact.
|
||||
McpConnectRefresh::Force => pool.force_reload_config_sources()?,
|
||||
}
|
||||
if let Some(handoff) = handoff {
|
||||
pool.adopt_handoff(handoff);
|
||||
}
|
||||
let eager = pool.eager_boot_server_names(&requested);
|
||||
let (pending, auth_errors) = pool.collect_pending_connects(Some(&eager));
|
||||
(
|
||||
|
||||
+71
-17
@@ -3211,6 +3211,30 @@ pub struct McpPool {
|
||||
/// unconnected on-demand server (`lazy = true` or plugin-contributed) is
|
||||
/// one nobody has asked for, not one mid-handshake.
|
||||
connecting: HashSet<String>,
|
||||
retired: bool,
|
||||
}
|
||||
|
||||
pub(crate) struct McpHandoff {
|
||||
workspace: Option<PathBuf>,
|
||||
backend: McpBackend,
|
||||
connections: Vec<(String, McpServerIdentity, McpConnection)>,
|
||||
}
|
||||
|
||||
type McpServerIdentity = (u64, Option<crate::plugins::types::PluginAuthority>);
|
||||
|
||||
fn mcp_server_identity(config: &McpConfig, name: &str) -> Option<McpServerIdentity> {
|
||||
config.servers.get(name).map(|server| {
|
||||
(
|
||||
hash_mcp_config(&McpConfig {
|
||||
timeouts: config.timeouts,
|
||||
servers: HashMap::from([(name.to_string(), server.clone())]),
|
||||
}),
|
||||
server
|
||||
.reviewed_plugin
|
||||
.as_ref()
|
||||
.map(|source| source.authority.clone()),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// One server's cooldown: when to try again, and what to say until then.
|
||||
@@ -3341,6 +3365,7 @@ impl McpPool {
|
||||
dynamic_servers: Arc::new(RwLock::new(HashMap::new())),
|
||||
needs_auth_servers: BTreeSet::new(),
|
||||
needs_auth_generation: 0,
|
||||
retired: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3450,7 +3475,7 @@ impl McpPool {
|
||||
}
|
||||
|
||||
fn server_allowed(&self, server: &str) -> bool {
|
||||
!Self::server_denied_by(&self.disallowed_tools, server)
|
||||
!self.retired && !Self::server_denied_by(&self.disallowed_tools, server)
|
||||
}
|
||||
|
||||
pub(crate) fn tool_allowed(&self, name: &str) -> bool {
|
||||
@@ -3458,6 +3483,10 @@ impl McpPool {
|
||||
}
|
||||
|
||||
fn require_server(&self, server: &str) -> Result<()> {
|
||||
anyhow::ensure!(
|
||||
!self.retired,
|
||||
"MCP server {server} belongs to a newer conversation; this conversation's MCP connections are closed"
|
||||
);
|
||||
anyhow::ensure!(
|
||||
self.server_allowed(server),
|
||||
"Failed to find MCP server: {server}"
|
||||
@@ -3618,28 +3647,14 @@ impl McpPool {
|
||||
if force {
|
||||
self.drop_all_connections("explicit config reload");
|
||||
} else {
|
||||
let server_identity = |config: &McpConfig, name: &str| {
|
||||
config.servers.get(name).map(|server| {
|
||||
(
|
||||
hash_mcp_config(&McpConfig {
|
||||
timeouts: config.timeouts,
|
||||
servers: HashMap::from([(name.to_string(), server.clone())]),
|
||||
}),
|
||||
server
|
||||
.reviewed_plugin
|
||||
.as_ref()
|
||||
.map(|source| source.authority.clone()),
|
||||
)
|
||||
})
|
||||
};
|
||||
let changed: HashSet<String> = self
|
||||
.config
|
||||
.servers
|
||||
.keys()
|
||||
.chain(new_config.servers.keys())
|
||||
.filter(|name| {
|
||||
server_identity(&self.config, name.as_str())
|
||||
!= server_identity(&new_config, name.as_str())
|
||||
mcp_server_identity(&self.config, name.as_str())
|
||||
!= mcp_server_identity(&new_config, name.as_str())
|
||||
})
|
||||
.cloned()
|
||||
.collect();
|
||||
@@ -4041,6 +4056,45 @@ impl McpPool {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn retire_for_handoff(&mut self) -> McpHandoff {
|
||||
self.retired = true;
|
||||
let connections = std::mem::take(&mut self.connections)
|
||||
.into_iter()
|
||||
.filter_map(|(name, connection)| {
|
||||
let identity = mcp_server_identity(&self.config, &name)?;
|
||||
(identity.1.is_none()
|
||||
&& connection.config().url.is_none()
|
||||
&& connection.is_transport_ready())
|
||||
.then_some((name, identity, connection))
|
||||
})
|
||||
.collect();
|
||||
McpHandoff {
|
||||
workspace: self.workspace.clone(),
|
||||
backend: self.backend,
|
||||
connections,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn adopt_handoff(&mut self, handoff: McpHandoff) -> usize {
|
||||
if self.retired || handoff.workspace != self.workspace || handoff.backend != self.backend {
|
||||
return 0;
|
||||
}
|
||||
let generation = self.current_catalog_generation();
|
||||
let mut adopted = 0;
|
||||
for (name, identity, mut connection) in handoff.connections {
|
||||
if mcp_server_identity(&self.config, &name).as_ref() != Some(&identity)
|
||||
|| !connection.is_transport_ready()
|
||||
{
|
||||
continue;
|
||||
}
|
||||
connection.catalog_generation = generation;
|
||||
if self.store_ready_connection(name, connection).is_ok() {
|
||||
adopted += 1;
|
||||
}
|
||||
}
|
||||
adopted
|
||||
}
|
||||
|
||||
/// Record a connect failure's auth classification. When the failure looks
|
||||
/// like a missing/expired OAuth login, the next model catalog offers the
|
||||
/// synthetic `mcp_<server>_authenticate` tool so the model can self-serve
|
||||
|
||||
@@ -572,6 +572,20 @@
|
||||
return { id: approvalId };
|
||||
}
|
||||
|
||||
function retireApproval(name, data) {
|
||||
if (name !== "approval.decided" && name !== "approval.timeout") return;
|
||||
const payload = eventPayload(data) || {};
|
||||
const approvalId = payload.approval_id || payload.id;
|
||||
if (!approvalId) return;
|
||||
const label = name === "approval.timeout"
|
||||
? "Timed out"
|
||||
: payload.cancelled ? "Cancelled" : "Decided: " + (payload.decision || "decided");
|
||||
for (const actions of document.querySelectorAll(".approval-actions")) {
|
||||
if (actions.dataset.approvalId !== String(approvalId)) continue;
|
||||
actions.innerHTML = "<span class='meta'>" + escapeHtml(label) + "</span>";
|
||||
}
|
||||
}
|
||||
|
||||
function pruneVisibleEvents() {
|
||||
const events = $("events");
|
||||
while (events.childElementCount > MAX_VISIBLE_EVENTS) {
|
||||
@@ -597,6 +611,7 @@
|
||||
|
||||
function appendEvent(name, data) {
|
||||
noteAgentEvent(name, data);
|
||||
retireApproval(name, data);
|
||||
const text = eventText(name, data);
|
||||
if (!text) return;
|
||||
const item = document.createElement("div");
|
||||
@@ -612,6 +627,7 @@
|
||||
if (approval) {
|
||||
const actions = document.createElement("div");
|
||||
actions.className = "approval-actions";
|
||||
actions.dataset.approvalId = String(approval.id);
|
||||
const allow = document.createElement("button");
|
||||
allow.className = "primary";
|
||||
allow.textContent = "Allow";
|
||||
|
||||
@@ -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.
|
||||
|
||||
+14
@@ -557,6 +557,20 @@ and `codewhale mcp connect` run in their own process, so a server they just
|
||||
reached from the shell still reads `not connected` inside a running session
|
||||
until that session connects it.
|
||||
|
||||
`/new`, and switching sessions from the resume picker without changing
|
||||
provider or workspace, keep user-configured stdio servers running: each live
|
||||
process moves to the new conversation without a restart, provided its config,
|
||||
the workspace and the MCP backend are unchanged. Anything the server holds in
|
||||
memory, such as a browser session and its logins, comes along; run
|
||||
`/mcp reload` for fresh servers. HTTP/SSE servers reconnect, so they pick up
|
||||
current credentials and `[network]` policy. Servers added at runtime and
|
||||
plugin-contributed servers are conversation capabilities and are closed.
|
||||
`/resume <id>`, `/load` and a provider or workspace switch start a new engine,
|
||||
and every server restarts. If an MCP call from the previous conversation is
|
||||
still running at the boundary, nothing is handed over and the servers restart
|
||||
as before; otherwise later calls from that conversation's background work fail
|
||||
instead of reconnecting.
|
||||
|
||||
An MCP-focused `tool_search` is also explicit discovery intent: search a
|
||||
configured server name (for example `engram`), an exact `mcp_<server>_...`
|
||||
name, or use `{"query":"mcp_.*","match":"regex"}`. The current turn's
|
||||
|
||||
@@ -369,6 +369,8 @@ codewhale mcp tools codewhale
|
||||
|
||||
会话启动时会在后台连接:每个已启用且被允许的用户配置服务器都会并发启动,不会阻塞第一个回合。把较重的服务器标记为 `lazy = true`,则在某个回合的 `allowed_tools`/`tools.always_load` 选择覆盖了它的 `mcp_<server>_*` 名称、某次模型调用解析到它的某个工具,或显式 `/mcp retry <name>` 请求它之前不会启动。标记为 `required` 的服务器即使是 `lazy` 也会在启动时连接,以便其失败在第一个回合之前暴露出来。尚未连接的服务器显示为 `not connected`,绝不会显示 `connecting`;`connecting` 标签只描述实际正在进行中的握手。OAuth 登录完成后,只有该服务器会自动重新连接。插件提供的服务器按需连接,除非标记为 `required` 或被会话的工具选择点名。
|
||||
|
||||
执行 `/new`,或在不更换提供商和工作区的情况下从恢复选择器切换会话时,用户配置的 stdio 服务器会继续运行:只要其配置、工作区和 MCP 后端未变,正在运行的进程会直接转交给新对话,无需重启。服务器保存在内存中的状态(例如浏览器会话及其登录)也会一并保留;需要全新的服务器时请运行 `/mcp reload`。HTTP/SSE 服务器会重新连接,从而使用当前的凭据和 `[network]` 策略。运行时添加的服务器和插件提供的服务器属于对话能力,会被关闭。`/resume <id>`、`/load` 以及切换提供商或工作区会启动新的引擎,所有服务器都会重启。如果交接时上一个对话仍有 MCP 调用在执行,则不转交任何连接,服务器照旧重启;否则,上一个对话的后台工作此后发起的调用会直接失败,而不会重新连接。
|
||||
|
||||
面向 MCP 的 `tool_search` 也是明确的发现请求:可搜索已配置的服务器名称
|
||||
(例如 `engram`)、完整的 `mcp_<server>_...` 名称,或使用
|
||||
`{"query":"mcp_.*","match":"regex"}`。当前回合的允许与拒绝范围先过滤服务器,
|
||||
|
||||
@@ -208,8 +208,8 @@
|
||||
"sources": [
|
||||
{
|
||||
"path": "docs/MCP.md",
|
||||
"sha256": "6190d0cca1798694216d51530942dbbf4f149b789f1323a1fcfac2cbe4856173",
|
||||
"content": "# MCP (External Tool Servers)\n\nIn the terminal, `/mcp` (also `/mcps`) opens **Extensions → MCP**. Enter opens the selected server’s recovery action or read-only details. An empty inventory offers server suggestions; browsing them installs nothing. Explicit subcommands such as `/mcp status`, `/mcp doctor`, `/mcp login`, and `/mcp add` retain their existing behavior.\n\n\n> 阅读简体中文版:[zh_hans/MCP.md](zh_hans/MCP.md)\n\ncodewhale can load additional tools via MCP (Model Context Protocol). MCP servers can be local stdio processes that the TUI starts, or remote URL-based servers that speak Streamable HTTP with legacy SSE fallback.\n\nBrowsing note:\n- `Web` is the canonical, deferred built-in browsing tool; it provides\n `search`, `fetch`, and `wait` actions when network policy permits.\n- `web_search`, `fetch_url`, and `wait_for_dev_server` are hidden replay-only\n aliases. New prompts and integrations should use `Web`.\n\nServer mode note:\n- `codewhale serve --mcp` runs the MCP stdio server.\n- `codewhale serve --http` runs the runtime HTTP/SSE API (separate mode).\n- `codewhale mcp-server` is an equivalent stdio entrypoint on the same\n consolidated runtime.\n\nIn 0.10.1, the old child-server aggregation proxy is removed. `mcp-server`\nnow uses the same native tool server as `serve --mcp`; it does not launch the\nlegacy `mcp.server_definitions` list. Saved definitions are left intact. Configure\nexternal servers in `mcp.json` for the existing MCP client, or connect to those\nservers directly from your external client. Native server write tools remain\nwithheld unless the operator explicitly permits them in its server configuration.\n\n## Setup wizard vs manual MCP setup (#3407)\n\nThe `/setup` hub includes an optional **Tools and MCP**\nstep. That step is discovery/readiness only:\n\n| Wizard can do | Still requires manual / explicit action |\n| --- | --- |\n| Show configured servers as `healthy` / `needs_config` / `off` | Start or connect MCP servers |\n| Report config path presence (global + project) | Write or edit `mcp.json` contents |\n| Safe static health probe (missing command/url, broken absolute path, missing bearer env) | `codewhale mcp validate`, live connect, OAuth login |\n| Point at safe on-ramps (`/mcp`, `codewhale mcp init`, `codewhale doctor`) | Install community skills, trust skills, enable plugins |\n| Share Hotbar source counts from the same skill/MCP adapters (#3399) | Bind Hotbar slots (Hotbar step / `H`) |\n| Record optional/`needs_action` setup_state without blocking first-run | Anything that spawns processes or installs packages |\n\nEmpty inventory is **not** an error: first-run users see “nothing configured\nyet, that’s fine.” Failing or incomplete configured servers surface as\n`needs_config` with an actionable hint and never block setup completion.\nEnumeration never executes MCP/plugin commands beyond the static probe.\nSummaries redact commands, args, env, headers, and tokens.\n\n`codewhale doctor` reports MCP/skills/tools/plugins health with the same\noptional-surface intent (paths, counts, static checks) so wizard and doctor\nstay consistent.\n\n## Plugin-contributed MCP\n\nA reviewed local plugin bundle may contribute MCP servers without creating a\nsecond transport or approval system. The servers use the same MCP manager,\ntool approval, resource, prompt, timeout, and network-policy paths documented\nhere, and appear under namespaced `<plugin>-<server>` identities.\n\nThe bundle boundary is intentionally stricter than user-authored `mcp.json`:\nunknown fields and ambiguous transports fail closed; stdio environment values\nmust be exact environment-source references; remote literal headers and\nsecret-bearing URLs are rejected; declared network hosts must exactly match\nthe normalized endpoint host set; and redirects remain on the reviewed origin.\nReviewed plugin remotes also bypass ambient HTTP proxy configuration entirely;\nproxy credentials and proxy-observed traffic are not part of the v1 review.\nThe plugin review discloses local host-user authority, structural argv,\nenvironment provenance, endpoint, auth source names, scopes, and tool filters\nwithout reading or printing secret values.\n\nTrust stages reviewed content but does not enable it. Enablement attaches that\nstaged snapshot to the current workspace's MCP pool. Disable, revoke, and other\ncross-process generation changes remove catalog entries, cancel in-flight\noperations, and terminate plugin stdio children. Source or staged-tree drift is\nfully revalidated before each dispatch/catalogue boundary and fails the next\nboundary closed; v0.9.1 does not continuously hash mutable trees during an\nalready-running call and therefore does not promise drift-triggered mid-call\ncancellation. MCP subscriptions are not exposed through plugin bundles. See\n[Plugin bundles](PLUGIN_BUNDLES.md) for the complete lifecycle contract.\n\n## Bootstrap MCP Config\n\nCreate a startLine truncated
|
||||
"sha256": "1246a1f9aa78314e0c7e304bb7219d5e15ba6d6b6bd060bbf08e77b5ea233285",
|
||||
"content": "# MCP (External Tool Servers)\n\nIn the terminal, `/mcp` (also `/mcps`) opens **Extensions → MCP**. Enter opens the selected server’s recovery action or read-only details. An empty inventory offers server suggestions; browsing them installs nothing. Explicit subcommands such as `/mcp status`, `/mcp doctor`, `/mcp login`, and `/mcp add` retain their existing behavior.\n\n\n> 阅读简体中文版:[zh_hans/MCP.md](zh_hans/MCP.md)\n\ncodewhale can load additional tools via MCP (Model Context Protocol). MCP servers can be local stdio processes that the TUI starts, or remote URL-based servers that speak Streamable HTTP with legacy SSE fallback.\n\nBrowsing note:\n- `Web` is the canonical, deferred built-in browsing tool; it provides\n `search`, `fetch`, and `wait` actions when network policy permits.\n- `web_search`, `fetch_url`, and `wait_for_dev_server` are hidden replay-only\n aliases. New prompts and integrations should use `Web`.\n\nServer mode note:\n- `codewhale serve --mcp` runs the MCP stdio server.\n- `codewhale serve --http` runs the runtime HTTP/SSE API (separate mode).\n- `codewhale mcp-server` is an equivalent stdio entrypoint on the same\n consolidated runtime.\n\nIn 0.10.1, the old child-server aggregation proxy is removed. `mcp-server`\nnow uses the same native tool server as `serve --mcp`; it does not launch the\nlegacy `mcp.server_definitions` list. Saved definitions are left intact. Configure\nexternal servers in `mcp.json` for the existing MCP client, or connect to those\nservers directly from your external client. Native server write tools remain\nwithheld unless the operator explicitly permits them in its server configuration.\n\n## Setup wizard vs manual MCP setup (#3407)\n\nThe `/setup` hub includes an optional **Tools and MCP**\nstep. That step is discovery/readiness only:\n\n| Wizard can do | Still requires manual / explicit action |\n| --- | --- |\n| Show configured servers as `healthy` / `needs_config` / `off` | Start or connect MCP servers |\n| Report config path presence (global + project) | Write or edit `mcp.json` contents |\n| Safe static health probe (missing command/url, broken absolute path, missing bearer env) | `codewhale mcp validate`, live connect, OAuth login |\n| Point at safe on-ramps (`/mcp`, `codewhale mcp init`, `codewhale doctor`) | Install community skills, trust skills, enable plugins |\n| Share Hotbar source counts from the same skill/MCP adapters (#3399) | Bind Hotbar slots (Hotbar step / `H`) |\n| Record optional/`needs_action` setup_state without blocking first-run | Anything that spawns processes or installs packages |\n\nEmpty inventory is **not** an error: first-run users see “nothing configured\nyet, that’s fine.” Failing or incomplete configured servers surface as\n`needs_config` with an actionable hint and never block setup completion.\nEnumeration never executes MCP/plugin commands beyond the static probe.\nSummaries redact commands, args, env, headers, and tokens.\n\n`codewhale doctor` reports MCP/skills/tools/plugins health with the same\noptional-surface intent (paths, counts, static checks) so wizard and doctor\nstay consistent.\n\n## Plugin-contributed MCP\n\nA reviewed local plugin bundle may contribute MCP servers without creating a\nsecond transport or approval system. The servers use the same MCP manager,\ntool approval, resource, prompt, timeout, and network-policy paths documented\nhere, and appear under namespaced `<plugin>-<server>` identities.\n\nThe bundle boundary is intentionally stricter than user-authored `mcp.json`:\nunknown fields and ambiguous transports fail closed; stdio environment values\nmust be exact environment-source references; remote literal headers and\nsecret-bearing URLs are rejected; declared network hosts must exactly match\nthe normalized endpoint host set; and redirects remain on the reviewed origin.\nReviewed plugin remotes also bypass ambient HTTP proxy configuration entirely;\nproxy credentials and proxy-observed traffic are not part of the v1 review.\nThe plugin review discloses local host-user authority, structural argv,\nenvironment provenance, endpoint, auth source names, scopes, and tool filters\nwithout reading or printing secret values.\n\nTrust stages reviewed content but does not enable it. Enablement attaches that\nstaged snapshot to the current workspace's MCP pool. Disable, revoke, and other\ncross-process generation changes remove catalog entries, cancel in-flight\noperations, and terminate plugin stdio children. Source or staged-tree drift is\nfully revalidated before each dispatch/catalogue boundary and fails the next\nboundary closed; v0.9.1 does not continuously hash mutable trees during an\nalready-running call and therefore does not promise drift-triggered mid-call\ncancellation. MCP subscriptions are not exposed through plugin bundles. See\n[Plugin bundles](PLUGIN_BUNDLES.md) for the complete lifecycle contract.\n\n## Bootstrap MCP Config\n\nCreate a startLine truncated
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user