Align chat reconciliation with Hermes Agent dashboard#665
Conversation
aa39d6c to
4ffb795
Compare
Greptile SummaryThis PR reworks the chat/session reconciliation path to use the Hermes Agent dashboard WebSocket as the primary event source, replacing the previous approach of merging partially-overlapping local DB state with renderer-side streaming state. The change covers local, remote HTTP, and SSH-tunneled sessions with consistent handling of reasoning, tool calls, errors, generated media, and restored sessions.
Confidence Score: 4/5Safe to merge for the core dashboard transport path; the SSH tunnel and remote model helpers have minor robustness gaps worth cleaning up in a follow-on. The PR is large (103 files, 1368 tests passing) and addresses the main fragility points raised in earlier review rounds — atomic compat patching, session_id validation, and text-based failure heuristics are all improved. The remaining issues are timing edge cases: the SSH tunnel marks itself running before health is confirmed when the port-open deadline expires quietly, and the remote model helpers incorrectly interpret HTTP 204 responses as failures. Neither breaks the happy path, but the SSH one could cause confusing multi-second delays in failure scenarios. src/main/ssh-tunnel.ts (tunnelRunning set before health check when port-open deadline expires), src/main/remote-models.ts (remoteRemoveModel / remoteUpdateModel interpret null 204 bodies as failures), src/renderer/src/screens/Chat/dashboardGatewayClient.ts (spurious onClose after failed connections) Important Files Changed
|
4ffb795 to
7bba49f
Compare
7bba49f to
5225dd8
Compare
| export async function startSshTunnel(config: SshConfig): Promise<void> { | ||
| if (tunnelStartPromise) return tunnelStartPromise; | ||
| tunnelStartPromise = startSshTunnelInner(config); | ||
| try { | ||
| await tunnelStartPromise; | ||
| } finally { | ||
| tunnelStartPromise = null; | ||
| } | ||
| } |
There was a problem hiding this comment.
When a second caller invokes
startSshTunnel with a different config while a start is already in flight, the new config is silently discarded and the caller receives the in-flight promise for the first config. The caller is told "success" but ends up holding a tunnel bound to the wrong host/port/key. A config change should either wait for the current start to finish and then start again, or abort the in-flight start before launching with the new config.
| export async function startSshTunnel(config: SshConfig): Promise<void> { | |
| if (tunnelStartPromise) return tunnelStartPromise; | |
| tunnelStartPromise = startSshTunnelInner(config); | |
| try { | |
| await tunnelStartPromise; | |
| } finally { | |
| tunnelStartPromise = null; | |
| } | |
| } | |
| export async function startSshTunnel(config: SshConfig): Promise<void> { | |
| // If an identical config is already starting, coalesce onto its promise. | |
| // If the config differs, wait for the in-flight start to settle first so we | |
| // don't race, then launch a fresh start with the new config. | |
| if (tunnelStartPromise) { | |
| await tunnelStartPromise.catch(() => undefined); | |
| if ( | |
| activeConfig?.host === config.host && | |
| activeConfig?.port === config.port && | |
| activeConfig?.user === config.user && | |
| activeConfig?.keyPath === config.keyPath && | |
| tunnelRunning | |
| ) { | |
| return; | |
| } | |
| } | |
| tunnelStartPromise = startSshTunnelInner(config); | |
| try { | |
| await tunnelStartPromise; | |
| } finally { | |
| tunnelStartPromise = null; | |
| } | |
| } |
Summary
This PR reworks Hermes One's chat/session reconciliation path to align much more closely with the direction taken by
hermes-agentand the Hermes Agent desktop app: use the dashboard transport and event stream as the primary source of truth, then adapt those events into Hermes One's richer chat UI instead of trying to repeatedly merge partially-overlapping local DB state with renderer-side streaming state.The main outcome is a more deterministic chat transcript pipeline for local, remote HTTP, and SSH-tunneled Hermes Agent sessions, including streamed reasoning, intermediate assistant output, tool calls, tool results, errors, generated media, pasted attachments, and restored sessions.
Rationale
Hermes One's previous reconciliation strategy tried to merge data modified by the desktop app with data modified independently by
hermes-agent. That worked for simple turns, but it was fragile around edge cases:The Hermes Agent desktop app avoids much of this class of bug by driving the UI from the Hermes Agent dashboard stream/session APIs. This PR follows that same direction while preserving Hermes One-specific functionality, including richer grouping, restored-session rendering, local compatibility paths, remote connection modes, and media display.
In short: Hermes One should not keep inventing a parallel reconciliation model when upstream Hermes Agent is converging on dashboard APIs and event streams. This PR moves Hermes One closer to that upstream-supported flow without dropping existing desktop features.
Major Changes
Dashboard chat transport and event adaptation
Session restoration and continuation
Remote HTTP and SSH dashboard modes
Model configuration behavior
Media and attachment handling
Test and lab infrastructure
Validation
Automated checks:
npm run typecheckpassednpm testpassedLive visual regression, driven through the Electron app via CDP/Playwright:
Representative reports from the validation run:
.sandbox/live-visual-regression/20260613222956/report.json.sandbox/live-visual-regression/20260613224308/report.jsonNotes