Repository navigation
fix(chat): announce the gateway session id on first turn activity - #983
aniruddhaadak80 wants to merge 1 commit into
Conversation
|
f5ddec9 to
53308b8
Compare
… working A new chat stayed out of the sidebar for the whole of its first run. The gateway transport only reported the session id from finish(), so the renderer learned it after the run completed, and the row the user was looking at did not exist until then. Long first runs made this look like a long wait (fathah#980). Route the three gateway signals that mean the turn is really under way -- a message delta, a reasoning delta, and a tool event -- through one helper that records the output and announces the stored session id. That is the same point at which sendMessageViaApi un-provisions a fresh id, so a turn that dies before producing anything still leaves no sidebar row behind. The stored id is announced rather than the live gateway id because it is what finish() reports and what state.db is keyed by, so the renderer and the session cache agree on the row to create. On the sidebar side, an open conversation with no loaded row now skips the refresh throttle: the click that opened New Chat is usually still inside the 5s window, so the refresh that would have listed the row was dropped and the row stayed invisible until the 60s poll. Switches to already-listed sessions stay throttled. Fixes fathah#980
53308b8 to
ed82553
Compare
| listed: ReadonlyArray<{ id: string }>, | ||
| liveSessionIds: ReadonlySet<string>, | ||
| ): boolean { | ||
| if (!sessionId || !liveSessionIds.has(sessionId)) return false; |
There was a problem hiding this comment.
Fast turns miss sidebar refresh
If a first turn finishes before the renderer processes its announced session ID, the run is no longer in loadingSessionIds. This check then treats the unlisted session as ineligible for a forced refresh, so the five-second throttle can drop the refresh and leave the row absent until a focus event or the 60-second poll.
Problem
Starting a new chat and sending a prompt does not put the conversation in the sidebar. The row only appears once the first run finishes, so the longer the run takes, the longer the session is unreachable. Switching agents and back makes it appear immediately, which is the tell that the row exists in
state.dband only the cache-backed sidebar list is behind.Fixes #980.
Root cause
Two independent gaps, both on the default local gateway transport.
1. The gateway transport never announced a fresh session id.
sendMessageViaApiand the runs transport callannounceSessionIdas soon as there is visible output, tool activity, or reasoning, and only report the id fromfinish()as a backstop.sendMessageViaTuiGatewayhad no announce call at all, sochat-session-startednever fired for a new gateway session and the renderer only learned the id fromchat-doneat the end of the run. Everything downstream keys off that id:useChatIPCcallssetHermesSessionId,LayoutderivescurrentSessionIdfrom it, and the sidebar refreshes oncurrentSessionId.2. The sidebar refresh that would have listed the row was being dropped.
SidebarRecentSessionsthrottles event-driven refreshes to one per 5s. The click that opens "New Chat" refreshes and arms that window, so the session-start refresh that follows a few seconds later is skipped. The next opportunity is the 60s interval or a window focus event, which is why the row showed up late rather than never.Change
src/main/hermes.ts- the three gateway signals that mean the turn is really under way (message delta, reasoning delta, tool event) now go through onenoteGatewayActivity()helper that records the output and announces the stored session id. The three sites previously sethasGatewayOutput = trueinline, so the flag and the announcement cannot drift apart.The stored id is announced rather than the live gateway id because that is the id
finish()reports and the idstate.dbis keyed by, so the renderer and the session cache agree on which row to create. It is announced at most once per turn.This deliberately does not announce at
session.create.lat.md/sidebar-navigation.mdrecords that fresh ids stay provisional until a turn produces output so a failed first turn does not leave a visible sidebar row, and the first-activity point is exactly wheresendMessageViaApidraws that line. A turn that dies before producing anything still announces nothing.src/renderer/src/screens/Layout/SidebarRecentSessions.tsx- new exportedneedsForcedSessionSync(sessionId, listed, liveSessionIds)reports an open conversation that belongs to a run still generating and has no loaded row, and the session-switch effect passes that as the existingforceflag onrefresh.Two conditions, not one, on purpose. A session missing from the loaded page is also true of any older conversation that sits past the first rows, and forcing a full
state.dbread on every switch into one would defeat the throttle the cache read exists to respect. Requiring the run to still be generating (theloadingSessionIdsprop, whichLayoutalready derives from the run list) restricts the un-throttled path to exactly the #980 case.There is no new timer and no new polling: the existing 60s interval and focus handler are untouched.
Tests
tests/gateway-session-announce.test.ts(new) drives the realsendMessageViaTuiGatewayover a mocked gateway WebSocket wheresession.createreturns a live id and a different stored id, so the tests prove which one the renderer is handed:session.create, before any outputonDonesrc/renderer/src/screens/Layout/SidebarRecentSessions.test.tsx(new) covers the predicate and the wiring, asserting sync counts as a delta from the settled mount state so it does not depend on how many opening syncs the component performs:lat.md/sidebar-navigation.mdgains a "Live first-turn rows" subsection under "Provisional fresh sessions", with@lat:code refs inhermes.tsandSidebarRecentSessions.tsxpointing at it.Commands
CI (
.github/workflows/ci.yml, Node 22,npm ci) runs exactly:which expand to:
npm run build(npm run typecheck && electron-vite build) is not part of CI.lat checkis also not part of CI.This branch was prepared without a local checkout, so I have no local test, typecheck, lint, or build results to report and am not claiming any. The two new test files are the only executable evidence, and CI is the first place they run. No existing test was changed.
Lifecycle caveats
state.dbbysyncSessionCache. First activity means the agent has already stored the prompt, so the generated title is normally available in the same sync. If a sync ever lands before that write, the row appears with the untitled fallback and picks up its generated title on the next poll.