Skip to content

fix(chat): announce the gateway session id on first turn activity - #983

Open
aniruddhaadak80 wants to merge 1 commit into
fathah:mainfrom
aniruddhaadak80:fix/sidebar-live-session-row
Open

aniruddhaadak80 wants to merge 1 commit into
fathah:mainfrom
aniruddhaadak80:fix/sidebar-live-session-row

Conversation

@aniruddhaadak80

@aniruddhaadak80 aniruddhaadak80 commented Sep 25, 2026 •

Copy link
Copy Markdown

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.db and 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. sendMessageViaApi and the runs transport call announceSessionId as soon as there is visible output, tool activity, or reasoning, and only report the id from finish() as a backstop. sendMessageViaTuiGateway had no announce call at all, so chat-session-started never fired for a new gateway session and the renderer only learned the id from chat-done at the end of the run. Everything downstream keys off that id: useChatIPC calls setHermesSessionId, Layout derives currentSessionId from it, and the sidebar refreshes on currentSessionId.

2. The sidebar refresh that would have listed the row was being dropped. SidebarRecentSessions throttles 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 one noteGatewayActivity() helper that records the output and announces the stored session id. The three sites previously set hasGatewayOutput = true inline, 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 id state.db is 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.md records 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 where sendMessageViaApi draws that line. A turn that dies before producing anything still announces nothing.

src/renderer/src/screens/Layout/SidebarRecentSessions.tsx - new exported needsForcedSessionSync(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 existing force flag on refresh.

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.db read on every switch into one would defeat the throttle the cache read exists to respect. Requiring the run to still be generating (the loadingSessionIds prop, which Layout already 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 real sendMessageViaTuiGateway over a mocked gateway WebSocket where session.create returns a live id and a different stored id, so the tests prove which one the renderer is handed:

  • no announcement after session.create, before any output
  • announcement on the first message delta, on the first reasoning delta, and on the first tool event
  • exactly one announcement across a whole turn, matching the id later reported by onDone

src/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:

  • a live run with no loaded row forces exactly one more sync inside the throttle window, and the row the sync returns then renders in the list
  • switching to an already-listed session adds no sync
  • resuming a session that is simply past the loaded page adds no sync either, so the throttle is not defeated for ordinary switches

lat.md/sidebar-navigation.md gains a "Live first-turn rows" subsection under "Provisional fresh sessions", with @lat: code refs in hermes.ts and SidebarRecentSessions.tsx pointing at it.

Commands

CI (.github/workflows/ci.yml, Node 22, npm ci) runs exactly:

npm run audit:prod
npm run typecheck
npm test
npm run lint

which expand to:

npm run typecheck   -> tsc --noEmit -p tsconfig.node.json --composite false
                       tsc --noEmit -p tsconfig.web.json --composite false
npm test            -> vitest run --maxWorkers=4
npm run lint        -> eslint --cache --max-warnings=0 .

npm run build (npm run typecheck && electron-vite build) is not part of CI. lat check is 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

  • A run that produces no visible output, no reasoning, and no tool event still does not appear until it completes. That is the provisional-id contract, not an oversight: announcing earlier is what would create rows for failed first turns.
  • The forced sync is gated on the run still generating. A first turn short enough to finish before the renderer re-renders falls back to the pre-existing 60s poll, same as before this change; that window is narrow and is not a regression.
  • The row is created from state.db by syncSessionCache. 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.
  • Switching to a different agent still forces an immediate reload, so the workaround in the issue keeps working unchanged.
  • Remote and SSH connections do not use this transport; they keep their existing remote/ssh cached-session paths.

@greptile-apps

greptile-apps Bot commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 5/5

[Medium risk] Announces chat session IDs earlier in the first turn.

The PR appears safe to merge, though a fast first turn can still leave its sidebar row delayed.

Findings

  1. P2 Fast turns miss sidebar refresh ▶

Summary

The PR announces the stored gateway session ID on first turn activity and lets the sidebar bypass its refresh throttle for an unlisted, generating session. It also updates the navigation documentation and adds gateway and sidebar tests.

Reviews (2) · Last reviewed commit: "fix(chat): announce the gateway session ..."

Comment thread src/renderer/src/screens/Layout/SidebarRecentSessions.tsx Outdated
Comment thread src/renderer/src/screens/Layout/SidebarRecentSessions.test.tsx
@aniruddhaadak80
aniruddhaadak80 force-pushed the fix/sidebar-live-session-row branch from f5ddec9 to 53308b8 Compare September 25, 2026 23:43
… 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
@aniruddhaadak80
aniruddhaadak80 force-pushed the fix/sidebar-live-session-row branch from 53308b8 to ed82553 Compare September 25, 2026 23:46
listed: ReadonlyArray<{ id: string }>,
liveSessionIds: ReadonlySet<string>,
): boolean {
if (!sessionId || !liveSessionIds.has(sessionId)) return false;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug New session never shows up in sessions until first prompt is finished by agent.

1 participant