Skip to content

fix(desktop): make the running-turn timer per-session#41182

Merged
teknium1 merged 1 commit into
mainfrom
fix/desktop-per-session-turn-timer
Jun 7, 2026
Merged

fix(desktop): make the running-turn timer per-session#41182
teknium1 merged 1 commit into
mainfrom
fix/desktop-per-session-turn-timer

Conversation

@teknium1

@teknium1 teknium1 commented Jun 7, 2026

Copy link
Copy Markdown
Contributor

Summary

The desktop running-turn timer is now per-session: leaving a busy session no longer "restarts its timer from zero."

Root cause: the statusbar turn timer read a single process-global $turnStartedAt, set/cleared only for the active session. With multiple same-profile sessions running at once (the multi-model setup @Da7_Tech hit after profile-scoped sessions), switching to session B reset the one shared clock, so session A's still-running turn appeared to restart.

Changes

  • app/types.ts: add turnStartedAt: number | null to ClientSessionState.
  • lib/chat-runtime.ts: initialize it to null in createClientSessionState.
  • use-message-stream.ts: seed/clear turnStartedAt on the per-session state at message.start, message.complete, error, the interrupted bail, and the session.info running-state path (seed-if-missing on busy, clear on stop). The global $turnStartedAt mirror stays gated on the active session.
  • use-session-state-cache.ts: on view-sync flush, mirror the focused session's turnStartedAt into the global atom the statusbar reads — so focusing a backgrounded turn restores its real elapsed time instead of zeroing it.
  • New test: use-session-state-cache.test.tsx — background turn keeps its clock and never touches the global atom; focused turn mirrors into the global; clearing the focused turn clears the global.

Not in scope

The agent loop never actually froze — every same-profile session runs in its own backend thread and background deltas are buffered per-session. This PR fixes the timer-reset symptom. The "no live thinking until you return, then it dumps at once" behaviour is inherent to a single-view transcript (only the focused session renders) and is left for a follow-up if we want live background-session indicators.

Validation

Before After
Leave a running session, return Timer reset to 0; looked frozen Timer kept counting; reflects true elapsed
Switch between two running sessions One shared clock thrashed between them Each session shows its own turn clock
tsc -b clean
vitest session hooks + store + chat-runtime 32 passed

Reported via Twitter (@Da7_Tech). Companion to #41120 (per-session /model scope) — that fixed the model cross-contamination; this fixes the timer half of the same report.

Infographic

per-session-turn-timer

The desktop statusbar turn timer read a single process-global $turnStartedAt,
set/cleared only for the active session. With multiple same-profile sessions
running at once, switching to session B reset the one shared clock, so
session A's still-running turn "restarted from zero" the moment you left it —
exactly the behaviour @Da7_Tech reported after the profile-scoped session work.

Move turnStartedAt onto ClientSessionState so each session owns its own turn
clock. The global atom now just mirrors whichever session is focused, written
on view-sync (the flush that already stages the active session's state). A
backgrounded turn keeps counting in its own cache entry, and focusing it
restores its real elapsed time instead of zeroing it.

Set/clear sites: message.start (seed), message.complete + error + interrupted
bail (clear), and the session.info running-state path (seed if missing / clear
on stop) so a turn that goes busy via session.info — e.g. resuming a session
that's already running — also gets a clock.

Note: the agent loop itself never froze — every same-profile session runs in
its own backend thread and background deltas are buffered per-session. This
fixes the timer-reset symptom; the "no live progress until you return" is
inherent to a single-view transcript and is out of scope here.
@github-actions

github-actions Bot commented Jun 7, 2026

Copy link
Copy Markdown
Contributor

🔎 Lint report: fix/desktop-per-session-turn-timer vs origin/main

ruff

Total: 0 on HEAD, 0 on base (➖ 0)

🆕 New issues: none

✅ Fixed issues: none

Unchanged: 0 pre-existing issues carried over.

ty (type checker)

Total: 10001 on HEAD, 10001 on base (➖ 0)

🆕 New issues: none

✅ Fixed issues: none

Unchanged: 5189 pre-existing issues carried over.

Diagnostics are surfaced as warnings — this check never fails the build.

@alt-glitch alt-glitch added type/bug Something isn't working P3 Low — cosmetic, nice to have labels Jun 7, 2026
@teknium1 teknium1 merged commit 9d72680 into main Jun 7, 2026
20 checks passed
@teknium1 teknium1 deleted the fix/desktop-per-session-turn-timer branch June 7, 2026 11:29
agogo233 added a commit to agogo233/hermes-agent that referenced this pull request Jun 8, 2026
* upstream/main: (430 commits)
  fix(yuanbao): bound ws.close() so an idle server can't stall shutdown ~5s (NousResearch#40607)
  docs: add Urdu translation of README (NousResearch#40578)
  fix(hindsight): send only new-turn delta on append retains instead of whole session (NousResearch#40605)
  feat(gateway): render terminal tool calls as native bash code blocks on markdown platforms (NousResearch#41215)
  feat(desktop): stop the chat viewport from following streaming output (NousResearch#41414)
  chore(release): map AlchemistChaos co-author email for NousResearch#40135 salvage
  fix(desktop): recover chat after sleep/wake by revalidating a stale remote backend
  fix(web): make _has_env config-aware so SEARXNG_URL auto-detect honors Hermes config
  fix(web): honor Hermes config-aware SEARXNG_URL lookup
  install.sh: hint at root-owned npm cache when desktop npm install fails (NousResearch#39688)
  fix(tools): percent-encode non-ascii URL components
  fix(skills): browse shows full catalog, not first 5000 (NousResearch#41413)
  feat(desktop+gateway): remote media relay — attach images/PDFs and display gateway images over the network
  feat(desktop): full tool-backend config (pickers + per-backend settings) in Settings (NousResearch#41232)
  hardening(api-server): scan cron prompts on REST create/update for parity with the agent tool
  fix: skip MCP preflight content-type probe on reconnect when already ready (NousResearch#40604)
  fix(kanban): sweep deferred scratch parent on non-scratch child completion + tests
  fix: defer scratch workspace cleanup when task has active children (NousResearch#33774)
  feat(onboarding): opt-in structured profile-build path on first contact (NousResearch#41114)
  feat(compression): temporal anchoring in compaction summaries (NousResearch#41102)
  test(discord): align clarify/model-picker tests with fail-closed component auth (NousResearch#41338)
  chore(release): map Dusk1e and LaPhilosophie for approval fail-closed salvage (NousResearch#33844, NousResearch#33866, NousResearch#30964)
  fix(discord): fail closed for component button auth when no allowlist set
  fix(feishu): fail closed for update prompt card actions
  fix(slack): re-check gateway auth on approval and slash-confirm buttons
  fix: guard int(os.getenv()) casts against malformed env vars (NousResearch#40598)
  fix: respect Honcho env var fallback in doctor and honcho status
  chore(release): add synapsesx to AUTHOR_MAP for NousResearch#40495 salvage
  fix(research): keep tool_call/tool_response pairs intact when compressing trajectories
  fix(simplex): accept display name in SIMPLEX_ALLOWED_USERS
  fix(desktop): make the running-turn timer per-session (NousResearch#41182)
  test(approval): regression for shell-escape denylist bypass (NousResearch#36846, NousResearch#36847)
  fix(security): strip shell escapes in denylist normalizer; fail-closed on missing approval module
  fix(stream+output-cap): guard empty streams and parse OpenRouter output-cap errors (NousResearch#40589)
  fix(desktop): bootstrap falls back to installed agent install.sh on GitHub 404
  feat(dashboard): change UI font from the theme picker, independent of theme (NousResearch#41145)
  fix(cli): return bool (not None) when a destructive-slash confirmation is cancelled (NousResearch#40583)
  fix(desktop): preserve configured base_url on same-provider model switch (NousResearch#41121)
  fix(desktop): stop bare-URL autolinker swallowing trailing emphasis asterisks (NousResearch#41093)
  fix(cron): bound the desktop run-history query to one job (NousResearch#41088)
  fix(desktop): scope in-session /model switch per-session, stop process-env leak (NousResearch#41120)
  chore: map bmoore210 author email for PR NousResearch#40550 salvage
  fix(desktop): scope session list to active profile + longer timeout
  fix: harden gateway startup and turn persistence
  fix(computer_use): honor custom vision routing
  fix(aux): honor model.default_headers on auxiliary client too (NousResearch#40033)
  fix(agent): honor model.default_headers for custom OpenAI-compatible providers (NousResearch#40033)
  docs(i18n): port deep-audit corrections to zh-Hans mirror (NousResearch#41104)
  fix(compression): don't overwrite the -1 post-compression sentinel in preflight seed (NousResearch#36718)
  chore(release): map singhsanidhya741@gmail.com to sanidhyasin (NousResearch#41094)
  ...
changman pushed a commit to changman/hermes-agent that referenced this pull request Jun 10, 2026
…1182)

The desktop statusbar turn timer read a single process-global $turnStartedAt,
set/cleared only for the active session. With multiple same-profile sessions
running at once, switching to session B reset the one shared clock, so
session A's still-running turn "restarted from zero" the moment you left it —
exactly the behaviour @Da7_Tech reported after the profile-scoped session work.

Move turnStartedAt onto ClientSessionState so each session owns its own turn
clock. The global atom now just mirrors whichever session is focused, written
on view-sync (the flush that already stages the active session's state). A
backgrounded turn keeps counting in its own cache entry, and focusing it
restores its real elapsed time instead of zeroing it.

Set/clear sites: message.start (seed), message.complete + error + interrupted
bail (clear), and the session.info running-state path (seed if missing / clear
on stop) so a turn that goes busy via session.info — e.g. resuming a session
that's already running — also gets a clock.

Note: the agent loop itself never froze — every same-profile session runs in
its own backend thread and background deltas are buffered per-session. This
fixes the timer-reset symptom; the "no live progress until you return" is
inherent to a single-view transcript and is out of scope here.
OmarB97 added a commit to OmarB97/hermes-agent that referenced this pull request Jun 10, 2026
…te renderer tests in CI (#160)

The running-turn timer's per-session clock (upstream NousResearch#41182) was clipped by the
upstream-sync's internal merge (e866446): ClientSessionState.turnStartedAt
and its tests survived, but all six seed/clear sites in use-message-stream.ts
and the view-sync mirror in use-session-state-cache.ts vanished — so the
statusbar timer never starts, and a backgrounded turn's elapsed time zeroes on
focus (the exact bug NousResearch#41182 fixed). Restored all seven sites to upstream's
shape. Renderer suite: 602/602 (first fully-green run; also passes under
TZ=UTC).

Because nothing in CI ran this suite, the clobber merged silently — same class
as the sleep/wake-recovery loss (#148). Add renderer-tests.yml gating
`vitest run --environment jsdom src` on every PR so the next one can't.

Co-authored-by: Omar Baradei <omar@kostudios.io>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

P3 Low — cosmetic, nice to have type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants