You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maka.apache.org by wy...@apache.org on 2026/08/27 13:58:55 UTC
(maka) branch main updated: fix(desktop): observe the first turn before admission (#4018)
This is an automated email from the ASF dual-hosted git repository.
M4n5ter pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/maka.git
The following commit(s) were added to refs/heads/main by this push:
new e0be705c0 fix(desktop): observe the first turn before admission (#4018)
e0be705c0 is described below
commit e0be705c015f3af973408da4b5c69845983e576e
Author: Wang <m4...@gmail.com>
AuthorDate: Thu Aug 27 21:58:50 2026 +0800
fix(desktop): observe the first turn before admission (#4018)
* fix(desktop): observe the first turn before admission
Generated-by: OpenAI Codex
* fix(desktop): bound first-send observation wait
Generated-by: OpenAI Codex
---
apps/desktop/e2e/streaming-remount.spec.ts | 31 +++++++-
.../app-shell-busy-race-settlement.test.ts | 8 +-
.../__tests__/app-shell-chat-actions-fixture.ts | 7 +-
.../__tests__/app-shell-first-send-cleanup.test.ts | 91 ++++++++++++++++++++++
apps/desktop/src/preload/preload.ts | 31 ++++++++
.../desktop/src/renderer/app-shell-chat-actions.ts | 52 +++++++------
apps/desktop/src/renderer/app-shell-effects.ts | 61 +++++++++++----
apps/desktop/src/renderer/app-shell.tsx | 57 +++++++++++++-
8 files changed, 298 insertions(+), 40 deletions(-)
diff --git a/apps/desktop/e2e/streaming-remount.spec.ts b/apps/desktop/e2e/streaming-remount.spec.ts
index 909e2ce01..998cd3c6b 100644
--- a/apps/desktop/e2e/streaming-remount.spec.ts
+++ b/apps/desktop/e2e/streaming-remount.spec.ts
@@ -25,6 +25,13 @@ import {
import type { Locator } from '@playwright/test';
import { COMPOSER_INPUT, ensureSidebarExpanded, expect, test } from './fixtures';
+interface SessionObservationLatchWindow extends Window {
+ /** E2E-only preload affordance; see the MAKA_E2E block in preload.ts. */
+ makaE2eLatch?: {
+ rejectNextSessionObservation(message: string): void;
+ };
+}
+
function sessionRow(sidebar: Locator, sessionId: string): Locator {
return sidebar.locator(`[data-session-id=${JSON.stringify(sessionId)}]`);
}
@@ -36,6 +43,28 @@ async function steerActiveTurn(composer: Locator, text: string): Promise<void> {
await composer.press('Shift+Enter');
}
+test('a failed first observation seed reconnects to the live Turn', async ({ window: page }) => {
+ const latchInstalled = await page.evaluate(() => {
+ const latch = (window as SessionObservationLatchWindow).makaE2eLatch;
+ if (!latch) return false;
+ latch.rejectNextSessionObservation('forced first observation failure');
+ return true;
+ });
+ expect(latchInstalled, 'the preload E2E latch is installed').toBe(true);
+
+ const composer = page.locator(COMPOSER_INPUT);
+ await composer.fill(FAKE_HOLD_OPEN_PROMPT);
+ await composer.press('Enter');
+
+ await expect(page.locator('.maka-bubble-streaming')).toContainText(
+ 'Fake backend waiting',
+ );
+ await page.getByRole('button', { name: '停止' }).click();
+ await expect(page.getByRole('button', { name: '重新生成' })).toHaveCount(1, {
+ timeout: 20_000,
+ });
+});
+
test('remounting a live surface leaves accumulated output settled', async ({
window: page,
}) => {
@@ -130,7 +159,7 @@ test('keeps a completed reply after an interrupted turn and conversation remount
await composer.fill(FAKE_HOLD_OPEN_PROMPT);
await composer.press('Enter');
await expect(page.locator('.maka-bubble-streaming')).toContainText(
- 'Fake backend waiting for the test to stop the Turn.',
+ 'Fake backend waiting',
);
const originalSessionId = await sidebar
.locator('[data-session-id]:has([aria-current="page"])')
diff --git a/apps/desktop/src/main/__tests__/app-shell-busy-race-settlement.test.ts b/apps/desktop/src/main/__tests__/app-shell-busy-race-settlement.test.ts
index b4661d00d..f74ee3687 100644
--- a/apps/desktop/src/main/__tests__/app-shell-busy-race-settlement.test.ts
+++ b/apps/desktop/src/main/__tests__/app-shell-busy-race-settlement.test.ts
@@ -459,8 +459,11 @@ describe('busy-raced send settlement', () => {
const actions = createAppShellChatActions({
...createActionsDeps(),
activeIdRef,
+ activateSessionForFirstSend: async (sessionId) => {
+ activated.push(sessionId);
+ activeIdRef.current = sessionId;
+ },
setActiveId: (sessionId: string | undefined) => {
- if (sessionId !== undefined) activated.push(sessionId);
activeIdRef.current = sessionId;
},
setLiveTurnBySession: turnState.setLiveTurnBySession,
@@ -500,6 +503,9 @@ describe('busy-raced send settlement', () => {
const actions = createAppShellChatActions({
...createActionsDeps(),
activeIdRef,
+ activateSessionForFirstSend: async (sessionId) => {
+ activeIdRef.current = sessionId;
+ },
setActiveId: (sessionId: string | undefined) => {
activeIdRef.current = sessionId;
},
diff --git a/apps/desktop/src/main/__tests__/app-shell-chat-actions-fixture.ts b/apps/desktop/src/main/__tests__/app-shell-chat-actions-fixture.ts
index 19c8e86b7..f7f2bbc22 100644
--- a/apps/desktop/src/main/__tests__/app-shell-chat-actions-fixture.ts
+++ b/apps/desktop/src/main/__tests__/app-shell-chat-actions-fixture.ts
@@ -92,9 +92,10 @@ export function createTransientState() {
}
export function createActionsDeps() {
+ const activeIdRef = { current: undefined as string | undefined };
return {
uiLocale: 'en' as const,
- activeIdRef: { current: undefined as string | undefined },
+ activeIdRef,
addPendingSessionAction: () => true,
captureComposerImportOwner: () => ({
sessionId: undefined,
@@ -107,6 +108,9 @@ export function createActionsDeps() {
markSessionReadLocally: () => undefined,
messageRetryPendingRef: { current: new Set<string>() },
refreshSessions: async () => [],
+ activateSessionForFirstSend: async (sessionId: string) => {
+ activeIdRef.current = sessionId;
+ },
setActiveId: () => undefined,
setMessageLoadErrorBySession: () => undefined,
setMessageRetryPendingBySession: () => undefined,
@@ -115,7 +119,6 @@ export function createActionsDeps() {
updateTransientMessage: () => undefined,
removeTransientMessage: () => undefined,
transcriptRangeRef: { current: undefined },
- setNavSelection: () => undefined,
setLiveTurnBySession: () => undefined,
setInteractionBySession: () => undefined,
showModelSetupToast: () => undefined,
diff --git a/apps/desktop/src/main/__tests__/app-shell-first-send-cleanup.test.ts b/apps/desktop/src/main/__tests__/app-shell-first-send-cleanup.test.ts
index bdaa74c16..2f688e976 100644
--- a/apps/desktop/src/main/__tests__/app-shell-first-send-cleanup.test.ts
+++ b/apps/desktop/src/main/__tests__/app-shell-first-send-cleanup.test.ts
@@ -301,6 +301,97 @@ describe('composer first-send cleanup', () => {
assert.deepEqual(removed, []);
});
+ it('waits for the new session observation before submitting its first message', async () => {
+ const observation = deferred<void>();
+ const order: string[] = [];
+ const activeIdRef = { current: undefined as string | undefined };
+ const restoreWindow = installWindow({
+ newTasks: {
+ create: async () => {
+ order.push('create');
+ return { id: 'session-1' };
+ },
+ },
+ sessions: {
+ submitMessage: async () => {
+ order.push('submit');
+ return { ok: true, attachments: [], skillInvocation: { loaded: [], failed: [] } };
+ },
+ },
+ });
+
+ try {
+ const sending = createAppShellChatActions({
+ ...createActionsDeps(),
+ activeIdRef,
+ activateSessionForFirstSend: async (sessionId) => {
+ order.push('observe');
+ activeIdRef.current = sessionId;
+ await observation.promise;
+ order.push('seeded');
+ },
+ }).send('hello');
+ await new Promise((resolve) => setImmediate(resolve));
+ assert.deepEqual(order, ['create', 'observe']);
+
+ observation.resolve();
+ assert.equal(await sending, true);
+ assert.deepEqual(order, ['create', 'observe', 'seeded', 'submit']);
+ } finally {
+ restoreWindow();
+ }
+ });
+
+ it('removes the unsent session and surfaces an observation barrier failure', async () => {
+ const activeIdRef = { current: undefined as string | undefined };
+ const removed: string[] = [];
+ const errors: string[] = [];
+ let submissions = 0;
+ const restoreWindow = installWindow({
+ newTasks: { create: async () => ({ id: 'session-1' }) },
+ sessions: {
+ submitMessage: async () => {
+ submissions += 1;
+ return { ok: true, attachments: [], skillInvocation: { loaded: [], failed: [] } };
+ },
+ remove: async (sessionId: string) => {
+ removed.push(sessionId);
+ },
+ },
+ });
+
+ try {
+ const actions = createAppShellChatActions({
+ ...createActionsDeps(),
+ activeIdRef,
+ activateSessionForFirstSend: async (sessionId) => {
+ activeIdRef.current = sessionId;
+ throw new Error('Timed out while preparing the new Session event stream');
+ },
+ setActiveId: (sessionId) => {
+ activeIdRef.current = sessionId;
+ },
+ isNewChatSendSurfaceActive: () => activeIdRef.current === undefined,
+ isShellSurfaceOwnerActive: (owner) =>
+ owner.navSection === 'sessions' && owner.sessionId === activeIdRef.current,
+ toastApi: {
+ error: (_title, description) => {
+ if (description) errors.push(description);
+ },
+ info: () => undefined,
+ },
+ });
+ assert.equal(await actions.send('hello'), false);
+ } finally {
+ restoreWindow();
+ }
+
+ assert.equal(submissions, 0);
+ assert.deepEqual(removed, ['session-1']);
+ assert.equal(activeIdRef.current, undefined);
+ assert.deepEqual(errors, ['The message could not be sent. Try again later.']);
+ });
+
it('leaves an EXISTING session alone when its send rejects', async () => {
// Only the session this send created is disposable. A send that fails in
// an open conversation must not delete the conversation.
diff --git a/apps/desktop/src/preload/preload.ts b/apps/desktop/src/preload/preload.ts
index 18ad2260e..e5550f015 100644
--- a/apps/desktop/src/preload/preload.ts
+++ b/apps/desktop/src/preload/preload.ts
@@ -3320,6 +3320,7 @@ if (process.env.MAKA_E2E === '1' && process.env.MAKA_E2E_USER_DATA_DIR) {
type LatchKey = 'newTasks.listInvocableSkills' | 'sessions.list' | 'settings.chunk';
const gates = new Map<LatchKey, { promise: Promise<void>; oneShot: boolean }>();
const releases = new Map<LatchKey, { resolve: () => void; reject: (error: Error) => void }>();
+ let nextSessionObservationError: Error | undefined;
const invocableSkillsWaiters = new Map<string, Array<() => void>>();
const waitForLatch = async (key: LatchKey): Promise<void> => {
const gate = gates.get(key);
@@ -3342,6 +3343,33 @@ if (process.env.MAKA_E2E === '1' && process.env.MAKA_E2E_USER_DATA_DIR) {
makaBridge.sessions.list.bind(makaBridge.sessions),
'sessions.list',
);
+ const subscribeSessionEvents = makaBridge.sessions.subscribeEvents.bind(makaBridge.sessions);
+ makaBridge.sessions.subscribeEvents = (
+ sessionId,
+ handler,
+ onSeeded,
+ onObservationSeed,
+ onSeedError,
+ ) => {
+ const nextError = nextSessionObservationError;
+ nextSessionObservationError = undefined;
+ if (!nextError) {
+ return subscribeSessionEvents(
+ sessionId,
+ handler,
+ onSeeded,
+ onObservationSeed,
+ onSeedError,
+ );
+ }
+ let disposed = false;
+ void Promise.resolve().then(() => {
+ if (!disposed) onSeedError?.(nextError);
+ });
+ return () => {
+ disposed = true;
+ };
+ };
const listInvocableSkills = makaBridge.skills.listInvocable.bind(makaBridge.skills);
makaBridge.skills.listInvocable = async (...args) => {
try {
@@ -3381,6 +3409,9 @@ if (process.env.MAKA_E2E === '1' && process.env.MAKA_E2E_USER_DATA_DIR) {
invocableSkillsWaiters.set(sessionId, waiters);
});
},
+ rejectNextSessionObservation(message: string) {
+ nextSessionObservationError = new Error(message);
+ },
release(key: LatchKey) {
releases.get(key)?.resolve();
releases.delete(key);
diff --git a/apps/desktop/src/renderer/app-shell-chat-actions.ts b/apps/desktop/src/renderer/app-shell-chat-actions.ts
index 69373f745..005013b32 100644
--- a/apps/desktop/src/renderer/app-shell-chat-actions.ts
+++ b/apps/desktop/src/renderer/app-shell-chat-actions.ts
@@ -157,6 +157,7 @@ export function createAppShellChatActions(deps: {
isShellSurfaceOwnerActive: (owner: ComposerImportOwner) => boolean;
messageRetryPendingRef: RefBox<Set<string>>;
refreshSessions: () => Promise<DesktopSessionSummary[]>;
+ activateSessionForFirstSend: (sessionId: string) => Promise<void>;
setActiveId: (sessionId: string | undefined) => void;
setMessageLoadErrorBySession: MessageLoadErrorUpdater;
setMessageRetryPendingBySession: BooleanRecordUpdater;
@@ -171,7 +172,6 @@ export function createAppShellChatActions(deps: {
) => void;
removeTransientMessage: (sessionId: string, messageId: string) => void;
transcriptRangeRef: RefBox<DesktopTranscriptRangeController | undefined>;
- setNavSelection: (selection: NavSelection) => void;
/** #646: arm the "正在处理…" indicator locally at send() — the model-wait
* window opens before any SessionEvent arrives (turn_started is not one). */
setLiveTurnBySession: LiveTurnRecordUpdater;
@@ -214,6 +214,7 @@ export function createAppShellChatActions(deps: {
isShellSurfaceOwnerActive,
messageRetryPendingRef,
refreshSessions,
+ activateSessionForFirstSend,
setActiveId,
setMessageLoadErrorBySession,
setMessageRetryPendingBySession,
@@ -222,7 +223,6 @@ export function createAppShellChatActions(deps: {
updateTransientMessage,
removeTransientMessage,
transcriptRangeRef,
- setNavSelection,
setLiveTurnBySession,
setInteractionBySession,
onInteractionChanged,
@@ -449,6 +449,7 @@ export function createAppShellChatActions(deps: {
unsentSessionId = undefined;
try {
await window.maka.sessions.remove(sessionId);
+ if (activeIdRef.current === sessionId) setActiveId(undefined);
await refreshSessions();
} catch {
// Best-effort: a failed cleanup must not replace the real error.
@@ -473,10 +474,19 @@ export function createAppShellChatActions(deps: {
orchestrationMode: newChatOrchestrationMode,
});
unsentSessionId = session.id;
+ optimisticSessionId = session.id;
// Consumed: the choice is now the created Session's, not the next
// draft's. A failed create leaves it in place so a retry keeps it.
if (newChatPermissionChoice) clearNewChatPermissionChoice();
- optimisticSessionId = session.id;
+ // Active-stream snapshots can only restore assistant segments that
+ // are still streaming. Wait until the observer is ready before the
+ // first admission so a completed segment in a still-running Turn
+ // cannot become durable text without live identity.
+ await activateSessionForFirstSend(session.id);
+ if (activeIdRef.current !== session.id) {
+ await discardUnsentSession();
+ return false;
+ }
optimisticMessageId = messageId;
showTransientUserMessage(
session.id,
@@ -522,8 +532,7 @@ export function createAppShellChatActions(deps: {
...(options.displayText ? { displayText: options.displayText } : {}),
...(quotes && quotes.length > 0 ? { quotes } : {}),
exactTurn,
- isSurfaceVisible: () =>
- Boolean(newChatOwner && isNewChatSendSurfaceActive(newChatOwner)),
+ isSurfaceVisible: () => activeIdRef.current === session.id,
});
if (submitted.kind === 'refused') {
await discardUnsentSession();
@@ -531,10 +540,6 @@ export function createAppShellChatActions(deps: {
}
unsentSessionId = undefined;
options.onSessionResolved?.(session.id);
- if (newChatOwner && isNewChatSendSurfaceActive(newChatOwner)) {
- setNavSelection({ section: 'sessions' });
- setActiveId(session.id);
- }
await refreshSessions();
return true;
}
@@ -607,6 +612,22 @@ export function createAppShellChatActions(deps: {
options.onSessionResolved?.(sessionId);
return true;
} catch (error) {
+ // Capture ownership before cleanup clears the optimistic Session. A
+ // barrier timeout belongs to the surface that was waiting for it, while
+ // navigation away still suppresses feedback.
+ const feedbackSessionId = optimisticSessionId ?? initialSessionId;
+ const diagnosticTarget = feedbackSessionId
+ ? { sessionId: feedbackSessionId }
+ : initialNewTaskTarget
+ ? { profileId: initialNewTaskTarget.profileId }
+ : undefined;
+ const sendStillOwnsCurrentSurface =
+ (feedbackSessionId !== undefined &&
+ isShellSurfaceOwnerActive({
+ ...sendOwner,
+ sessionId: feedbackSessionId,
+ })) ||
+ (newChatOwner !== null && isNewChatSendSurfaceActive(newChatOwner));
await discardUnsentSession();
if (optimisticSessionId && optimisticMessageId) {
removeOptimisticUserMessage(optimisticSessionId, optimisticMessageId);
@@ -630,19 +651,6 @@ export function createAppShellChatActions(deps: {
// The owner MOVES on an optimistic create: the send began on the new-chat
// surface and the app is now on the session it just made, so the id is
// taken from the flight and only the section comes from the capture.
- const feedbackSessionId = optimisticSessionId ?? initialSessionId;
- const diagnosticTarget = feedbackSessionId
- ? { sessionId: feedbackSessionId }
- : initialNewTaskTarget
- ? { profileId: initialNewTaskTarget.profileId }
- : undefined;
- const sendStillOwnsCurrentSurface =
- (feedbackSessionId !== undefined &&
- isShellSurfaceOwnerActive({
- ...sendOwner,
- sessionId: feedbackSessionId,
- })) ||
- (newChatOwner !== null && isNewChatSendSurfaceActive(newChatOwner));
if (!sendStillOwnsCurrentSurface) return false;
if (isNoRealConnectionError(error)) {
const reason = noRealConnectionReasonFromError(error);
diff --git a/apps/desktop/src/renderer/app-shell-effects.ts b/apps/desktop/src/renderer/app-shell-effects.ts
index 20c83217a..6277ff143 100644
--- a/apps/desktop/src/renderer/app-shell-effects.ts
+++ b/apps/desktop/src/renderer/app-shell-effects.ts
@@ -425,8 +425,11 @@ export function useActiveSessionEvents(options: {
useLayoutEffect(() => {
if (!activeId) return;
- const observationGeneration = beginObservationSeed(activeId);
let disposed = false;
+ let observationAttempt = 0;
+ let observationFailures = 0;
+ let observationRetryTimer: ReturnType<typeof globalThis.setTimeout> | undefined;
+ let unsubscribeSessionEvents = () => {};
const transcript = new DesktopTranscriptRangeStore(activeId);
const subscribedAt = Date.now();
options.setMessageLoadErrorBySession((current) => {
@@ -465,24 +468,56 @@ export function useActiveSessionEvents(options: {
applyReadError(activeId, error, () => disposed);
});
options.transcriptRangeRef.current = controller;
- const unsubscribe = window.maka.sessions.subscribeEvents(
- activeId,
- (event) => {
- handleSessionEvent(activeId, event);
- },
- () => completeObservationSeed(activeId, observationGeneration),
- (phase) => {
- if (phase === 'pending') beginObservationSeed(activeId);
- else completeObservationSeed(activeId);
- },
- );
+ const subscribeSessionEvents = () => {
+ const attempt = ++observationAttempt;
+ const observationGeneration = beginObservationSeed(activeId);
+ let unsubscribeRequested = false;
+ let unsubscribeCurrent = () => {
+ unsubscribeRequested = true;
+ };
+ const unsubscribe = window.maka.sessions.subscribeEvents(
+ activeId,
+ (event) => {
+ if (attempt !== observationAttempt) return;
+ handleSessionEvent(activeId, event);
+ },
+ () => {
+ if (attempt !== observationAttempt) return;
+ observationFailures = 0;
+ completeObservationSeed(activeId, observationGeneration);
+ },
+ (phase) => {
+ if (attempt !== observationAttempt) return;
+ if (phase === 'pending') beginObservationSeed(activeId);
+ else completeObservationSeed(activeId);
+ },
+ () => {
+ if (disposed || attempt !== observationAttempt) return;
+ unsubscribeCurrent();
+ observationFailures += 1;
+ const retryDelayMs = Math.min(100 * (2 ** (observationFailures - 1)), 2_000);
+ observationRetryTimer = globalThis.setTimeout(() => {
+ observationRetryTimer = undefined;
+ if (!disposed && attempt === observationAttempt) subscribeSessionEvents();
+ }, retryDelayMs);
+ },
+ );
+ unsubscribeCurrent = unsubscribe;
+ unsubscribeSessionEvents = unsubscribe;
+ if (unsubscribeRequested) unsubscribe();
+ };
+ subscribeSessionEvents();
return () => {
disposed = true;
+ observationAttempt += 1;
+ if (observationRetryTimer !== undefined) {
+ globalThis.clearTimeout(observationRetryTimer);
+ }
if (options.transcriptRangeRef.current?.store === transcript) {
options.transcriptRangeRef.current = undefined;
}
void controller.close();
- unsubscribe();
+ unsubscribeSessionEvents();
markSessionEventStreamClosed(activeId);
};
}, [activeId]);
diff --git a/apps/desktop/src/renderer/app-shell.tsx b/apps/desktop/src/renderer/app-shell.tsx
index 7a8fc2e9d..9b91c73ea 100644
--- a/apps/desktop/src/renderer/app-shell.tsx
+++ b/apps/desktop/src/renderer/app-shell.tsx
@@ -258,6 +258,13 @@ function newTaskDraftKey(target: {
* assistant stream slot when the primary post-commit signal is missed.
*/
const SETTLE_FALLBACK_GRACE_MS = 1000;
+const FIRST_SEND_OBSERVATION_TIMEOUT_MS = 30_000;
+type FirstSendObservationWaiter = {
+ promise: Promise<void>;
+ resolve: () => void;
+ reject: (error: Error) => void;
+ timeoutId: ReturnType<typeof globalThis.setTimeout>;
+};
/**
* Module surfaces that own their whole column and render no workspace toolbar.
* This used to be a `display: none` rule keyed on the detail panel's
@@ -1744,6 +1751,48 @@ function AppShellContent({
[sessions, localProjects, sessionNavigation.commands],
);
+ const firstSendObservationWaitersRef = useRef(
+ new Map<string, FirstSendObservationWaiter>(),
+ );
+ const activateSessionForFirstSend = useCallback((sessionId: string): Promise<void> => {
+ let waiter = firstSendObservationWaitersRef.current.get(sessionId);
+ if (!waiter) {
+ let resolve!: () => void;
+ let reject!: (error: Error) => void;
+ const promise = new Promise<void>((resolvePromise, rejectPromise) => {
+ resolve = resolvePromise;
+ reject = rejectPromise;
+ });
+ let created!: FirstSendObservationWaiter;
+ const timeoutId = globalThis.setTimeout(() => {
+ if (firstSendObservationWaitersRef.current.get(sessionId) !== created) return;
+ firstSendObservationWaitersRef.current.delete(sessionId);
+ created.reject(new Error('Timed out while preparing the new Session event stream'));
+ }, FIRST_SEND_OBSERVATION_TIMEOUT_MS);
+ created = { promise, resolve, reject, timeoutId };
+ waiter = created;
+ firstSendObservationWaitersRef.current.set(sessionId, waiter);
+ }
+ setNavSelection({ section: 'sessions' });
+ setActiveId(sessionId);
+ return waiter.promise;
+ }, [setActiveId, setNavSelection]);
+ useEffect(() => {
+ for (const [sessionId, waiter] of firstSendObservationWaitersRef.current) {
+ if (sessionId === activeId) continue;
+ firstSendObservationWaitersRef.current.delete(sessionId);
+ globalThis.clearTimeout(waiter.timeoutId);
+ waiter.reject(new Error('The new Session was left before its event stream became ready'));
+ }
+ }, [activeId]);
+ useEffect(() => () => {
+ for (const [sessionId, waiter] of firstSendObservationWaitersRef.current) {
+ firstSendObservationWaitersRef.current.delete(sessionId);
+ globalThis.clearTimeout(waiter.timeoutId);
+ waiter.reject(new Error('The app closed before the new Session event stream became ready'));
+ }
+ }, []);
+
const { applyE2eFixture } = useStableActions(createAppShellE2eFixtureActions, {
openSettingsSection,
refreshSessions,
@@ -1778,6 +1827,7 @@ function AppShellContent({
isShellSurfaceOwnerActive,
messageRetryPendingRef,
refreshSessions,
+ activateSessionForFirstSend,
setActiveId,
setMessageLoadErrorBySession,
setMessageRetryPendingBySession,
@@ -1786,7 +1836,6 @@ function AppShellContent({
updateTransientMessage,
removeTransientMessage,
transcriptRangeRef,
- setNavSelection,
setLiveTurnBySession,
setInteractionBySession,
onInteractionChanged: markInteractionChanged,
@@ -2312,6 +2361,12 @@ function AppShellContent({
const next = completeLiveContentSeed(current, sessionId, expected);
activeEventSeedRef.current = next;
setActiveEventSeed(next);
+ const firstSendWaiter = firstSendObservationWaitersRef.current.get(sessionId);
+ if (firstSendWaiter) {
+ firstSendObservationWaitersRef.current.delete(sessionId);
+ globalThis.clearTimeout(firstSendWaiter.timeoutId);
+ firstSendWaiter.resolve();
+ }
void retireCancelledTransientMessages(sessionId);
};
useActiveSessionEvents({
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org