From 837977ba9f61be0dd45f6fbd5bf8f496a0247e67 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Fri, 13 Nov 2026 05:33:17 +0800 Subject: [PATCH 1/2] fix(chat): keep steward turn observation across views Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../dashboard/src/views/dashboard-page.tsx | 7 +++- .../conversation-activity.mjs | 38 ++++++++++++++----- 2 files changed, 33 insertions(+), 12 deletions(-) diff --git a/apps/presentation/dashboard/src/views/dashboard-page.tsx b/apps/presentation/dashboard/src/views/dashboard-page.tsx index f738472bde..7091e68865 100644 --- a/apps/presentation/dashboard/src/views/dashboard-page.tsx +++ b/apps/presentation/dashboard/src/views/dashboard-page.tsx @@ -1819,13 +1819,16 @@ function PersonalGoalHome({ }); }, [conversationHistory.history, contextId, selectedAgent.label, runtimeAgents]); + // Switching steward views keeps the same conversation. A Goal's overview + // still cannot initiate recovery for a conversation that was not opened. + const mayRecoverConversation = !selectedGoal || selectedView === "chat"; useEffect(() => { if (readOnly || appEntryBlocked) return; if (!selectedAgent.available) return; if (!conversationHistory.connectionKey || !conversationHistory.history) return; // Browsing a Goal (including palette navigation) must not create/resume // a session. Explicit Chat entry or sending through the composer owns that. - if (selectedGoal && selectedView !== "chat") return; + if (!mayRecoverConversation) return; const readHistory = conversationHistory.history; const targetContextId = contextId; const sessionKey = `${targetContextId}:${selectedAgent.agentId}`; @@ -2098,7 +2101,7 @@ function PersonalGoalHome({ retireRecoveryObservation?.(); window.clearTimeout(handoffRetryTimer); }; - }, [conversationHistory.connectionKey, contextId, model.goals[0]?.goalId, readOnly, appEntryBlocked, selectedView, selectedGoal?.goalId, selectedAgent.agentId, selectedAgent.available, selectedAgent.label, selectedAgents, turnRecoveryRequest]); + }, [conversationHistory.connectionKey, contextId, model.goals[0]?.goalId, readOnly, appEntryBlocked, mayRecoverConversation, selectedGoal?.goalId, selectedAgent.agentId, selectedAgent.available, selectedAgent.label, selectedAgents, turnRecoveryRequest]); // The projection deliberately outlives the mounted view. Its caller may be // running for a Goal the owner has already left, and the recovery stream's diff --git a/examples/personal-workspace-browser/conversation-activity.mjs b/examples/personal-workspace-browser/conversation-activity.mjs index 7a2794865d..4159b3e00a 100644 --- a/examples/personal-workspace-browser/conversation-activity.mjs +++ b/examples/personal-workspace-browser/conversation-activity.mjs @@ -213,18 +213,36 @@ export const conversationActivityScenario = { await makeStorageUnavailable("storage unavailable"); await assertStorageUnavailable("storage unavailable"); await composer.fill("先检查最新证据。"); - await page.getByRole("navigation", { name: "管家视图" }).getByRole("button", { name: "总览", exact: true }).click(); - await page.getByRole("navigation", { name: "管家视图" }).getByRole("button", { name: /^(Chat|对话)$/ }).click(); - assert.equal(await composer.inputValue(), "先检查最新证据。", "Blocked storage preserves the page draft across manager views"); + // A slow resume cannot turn a correction into a fresh message while + // the same manager conversation still owns an active Turn. + let releaseResume; + const resumeGate = new Promise(resolve => { releaseResume = resolve; }); + const delayResume = async route => { + const request = route.request(); + const body = request.method() === "POST" ? request.postDataJSON() : null; + if (body?.context_kind === "manager" && body.mode === "resume_latest") await resumeGate; + return route.fallback(); + }; + await page.route("**/api/chat/sessions", delayResume); + const streamsBeforeViewChange = observations.length; const dispatchedBeforeStorageFailure = adjustments.length; const turnsBeforeStorageFailure = api.turnRequests.length; - await composer.focus(); - await page.keyboard.press("Meta+Enter"); - await waitForFeedback("调整尚未发送", "Storage failure did not report a retained steering draft"); - assert.equal(adjustments.length, dispatchedBeforeStorageFailure, "Storage failure cannot dispatch steering"); - assert.equal(api.turnRequests.length, turnsBeforeStorageFailure, "Storage failure cannot queue a new conversation Turn"); - assert.equal(await composer.inputValue(), "先检查最新证据。"); - await page.screenshot({path: resolve(outputDir, "conversation-steering-storage-unavailable.png"), animations: "disabled"}); + try { + await page.getByRole("navigation", { name: "管家视图" }).getByRole("button", { name: "总览", exact: true }).click(); + await page.getByRole("navigation", { name: "管家视图" }).getByRole("button", { name: /^(Chat|对话)$/ }).click(); + assert.equal(await composer.inputValue(), "先检查最新证据。", "Blocked storage preserves the page draft across manager views"); + await composer.focus(); + await page.keyboard.press("Meta+Enter"); + await waitForFeedback("调整尚未发送", "Storage failure did not report a retained steering draft"); + assert.equal(adjustments.length, dispatchedBeforeStorageFailure, "Storage failure cannot dispatch steering"); + assert.equal(api.turnRequests.length, turnsBeforeStorageFailure, "Storage failure cannot queue a new conversation Turn"); + assert.equal(await composer.inputValue(), "先检查最新证据。"); + await page.screenshot({path: resolve(outputDir, "conversation-steering-storage-unavailable.png"), animations: "disabled"}); + assert.equal(observations.length, streamsBeforeViewChange, "Switching manager views keeps the original observer"); + } finally { + releaseResume(); + await page.unroute("**/api/chat/sessions", delayResume); + } await restoreStorage(); await composer.fill("先检查最新证据,再继续。"); await composer.focus(); From cb648901e1b21a2f9e7211eb2b28f33be0d31414 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Fri, 13 Nov 2026 02:33:03 +0800 Subject: [PATCH 2/2] test(lark): mark the imported transcript fixture for Ruff (cherry picked from commit a32597fd74016b98163c001a3a3dc5d69587689c) Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- tests/test_lark_private_conversations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_lark_private_conversations.py b/tests/test_lark_private_conversations.py index b3e1d8cc59..0e35a7c3b1 100644 --- a/tests/test_lark_private_conversations.py +++ b/tests/test_lark_private_conversations.py @@ -2813,7 +2813,7 @@ def test_app_messages_and_answers_reach_the_original_chat_once_without_new_execu runtime.close() -def test_app_projection_discovers_a_restored_transcript_with_the_same_size_and_mtime(ordinary): +def test_app_projection_discovers_a_restored_transcript_with_the_same_size_and_mtime(ordinary): # noqa: F811 import os store, runtime, provider, transport, original, turns, before = _app_projection_journey(ordinary)