mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-07-12 21:02:17 +02:00
fix race condition in chat history
This commit is contained in:
parent
9503682629
commit
930089b218
3 changed files with 55 additions and 4 deletions
|
|
@ -702,6 +702,19 @@ export function startSessionsWatcher(): void {
|
|||
sessionsWatcher = sessionBus.subscribe((event) => emitSessionEvent(event));
|
||||
}
|
||||
|
||||
// The renderer window is created before the session-index startup scan
|
||||
// finishes, so an early sessions:list could observe a partially built index
|
||||
// (the scan runs oldest-first — exactly the newest chats would be missing).
|
||||
// sessions:list awaits this deferred; main.ts resolves it when the scan
|
||||
// settles (success or failure, so the list never hangs).
|
||||
let resolveSessionsIndexReady: () => void;
|
||||
const sessionsIndexReady = new Promise<void>((resolve) => {
|
||||
resolveSessionsIndexReady = resolve;
|
||||
});
|
||||
export function markSessionsIndexReady(): void {
|
||||
resolveSessionsIndexReady();
|
||||
}
|
||||
|
||||
let servicesWatcher: (() => void) | null = null;
|
||||
export async function startServicesWatcher(): Promise<void> {
|
||||
if (servicesWatcher) {
|
||||
|
|
@ -938,6 +951,7 @@ export function setupIpcHandlers() {
|
|||
return { sessionId };
|
||||
},
|
||||
'sessions:list': async () => {
|
||||
await sessionsIndexReady;
|
||||
return { sessions: container.resolve<ISessions>('sessions').listSessions() };
|
||||
},
|
||||
'sessions:get': async (_event, args) => {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { app, BrowserWindow, desktopCapturer, protocol, net, shell, session, typ
|
|||
import path from "node:path";
|
||||
import {
|
||||
setupIpcHandlers,
|
||||
startRunsWatcher, startSessionsWatcher,
|
||||
startRunsWatcher, startSessionsWatcher, markSessionsIndexReady,
|
||||
startChannelsWatcher,
|
||||
startCodeSessionStatusWatcher,
|
||||
startServicesWatcher,
|
||||
|
|
@ -411,9 +411,16 @@ app.whenReady().then(async () => {
|
|||
console.error('[runs-migration] pass failed:', error);
|
||||
}
|
||||
|
||||
// New runtime: build the in-memory session index (startup scan) before the
|
||||
// renderer can list sessions, then forward the session bus to windows.
|
||||
await container.resolve<ISessions>('sessions').initialize();
|
||||
// New runtime: build the in-memory session index (startup scan), then
|
||||
// forward the session bus to windows. The renderer window is already up and
|
||||
// may have called sessions:list — that handler blocks on
|
||||
// markSessionsIndexReady, which must fire even if the scan throws so the
|
||||
// list never hangs.
|
||||
try {
|
||||
await container.resolve<ISessions>('sessions').initialize();
|
||||
} finally {
|
||||
markSessionsIndexReady();
|
||||
}
|
||||
startSessionsWatcher();
|
||||
|
||||
// Mobile channels (WhatsApp/Telegram bridge): needs the session index, so
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import { cn } from '@/lib/utils';
|
|||
import { MarkdownEditor, type MarkdownEditorHandle } from './components/markdown-editor';
|
||||
import { ChatSidebar } from './components/chat-sidebar';
|
||||
import { useSessionChat } from '@/hooks/useSessionChat';
|
||||
import { subscribeSessionFeed } from '@/lib/session-chat/feed';
|
||||
import { ChatHeader } from './components/chat-header';
|
||||
import { ChatEmptyState } from './components/chat-empty-state';
|
||||
import { ChatInputWithMentions, type CallPreset, type PermissionMode, type StagedAttachment } from './components/chat-input-with-mentions';
|
||||
|
|
@ -2254,6 +2255,35 @@ function App() {
|
|||
loadRuns()
|
||||
}, [loadRuns])
|
||||
|
||||
// Keep the runs list live: the session index publishes index-changed on
|
||||
// every write (session created, turn settled, title change, delete), so the
|
||||
// list stays current without re-fetching.
|
||||
useEffect(() => {
|
||||
return subscribeSessionFeed((event) => {
|
||||
if (event.kind !== 'index-changed') return
|
||||
setRuns((prev) => {
|
||||
if (event.entry === null) {
|
||||
return prev.filter((run) => run.id !== event.sessionId)
|
||||
}
|
||||
const next: RunListItem = {
|
||||
id: event.entry.sessionId,
|
||||
title: event.entry.title ?? 'New chat',
|
||||
createdAt: event.entry.createdAt,
|
||||
modifiedAt: event.entry.updatedAt,
|
||||
agentId: event.entry.lastAgentId ?? 'copilot',
|
||||
}
|
||||
// Re-sort: chat-header and home-view slice the top of this list
|
||||
// without sorting, so it must stay newest-first like sessions:list.
|
||||
const recency = (run: RunListItem) => {
|
||||
const ms = new Date(run.modifiedAt).getTime()
|
||||
return Number.isNaN(ms) ? 0 : ms
|
||||
}
|
||||
return [...prev.filter((run) => run.id !== next.id), next]
|
||||
.sort((a, b) => recency(b) - recency(a))
|
||||
})
|
||||
})
|
||||
}, [])
|
||||
|
||||
const [bgTaskSummaries, setBgTaskSummaries] = useState<Array<{
|
||||
slug: string
|
||||
name: string
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue