Merge pull request #671 from rowboatlabs/fix_chat_history

fix race condition in chat history
This commit is contained in:
arkml 2026-07-05 23:36:39 +05:30 committed by GitHub
commit 1c380c33ed
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 55 additions and 4 deletions

View file

@ -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) => {

View file

@ -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

View file

@ -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