Commit Graph

28271 Commits

Author SHA1 Message Date
KIAgent01 c0e07837c2 fix(desktop): clear stale turn state after lost stream events
Three safeguards keep finished chats from looking busy: tool rows seal on turn settle, vanished runtimes clear awaiting state and open tool parts, and late stream events no longer land in a freshly opened session.
2026-08-14 20:40:24 -07:00
Hermes Agent 12d99312aa chore: map contributor email for nicolasdmolina 2026-08-14 20:40:15 -07:00
Hermes Agent 7d74375b0b fix(desktop): keep session-list scrollbar clickable beside pane sash
The pane-resize sash's 9px grab band was centered on the split boundary,
so ~4.5px reached into the leading pane and sat exactly on top of the
session list's 4px scrollbar — the pointer always hit the sash (cursor
flipped to col-resize) and the scrollbar thumb was unclickable/undraggable.

Make the grab band asymmetric: 1px into the leading pane, 7px into the
trailing one. The scrollbar regains its full hit area while the sash stays
an easy 8px target; the hairline and hover strip are repositioned onto the
actual boundary.

Fixes #79157
2026-08-14 20:40:15 -07:00
Nicky Molina 6f64a2c631 fix(desktop): reanchor transcript on window focus 2026-08-14 20:40:15 -07:00
konsisumer bb45dc06c3 fix(desktop): stabilize virtual session scrolling 2026-08-14 20:40:15 -07:00
Teknium fceab28602 fix(desktop): cold start no longer hides Cloud agents until Portal re-login
Fixes #73495. Two cold-start defects made the configured Hermes Cloud
agent vanish after a Desktop restart even though the persisted Portal
session was still renewable:

1. hasLivePortalSession() trusted the FIRST cookies.get() on the lazy
   `persist:` partition. It now reuses the warmOauthCookieStore()
   warm-up + bounded reread that hasLiveOauthSession() gained in
   PR #67769, so a single hydration false-negative no longer clears the
   agent list and flips the panel to signed-out.

2. Discovery required the short-lived `privy-token` access cookie but
   treated its absence as a full interactive re-login, even when the
   30-day `privy-session` / `privy-refresh-token` renewal cookies
   survived the process exit. New cookiesHavePrivyAccessToken() splits
   "signed in (renewable)" from "discovery can succeed right now";
   discoverCloudAgents() and cloudAgentSilentSignIn() now mint a fresh
   access token via one bounded, hidden, deadline-capped portal load
   (renewPortalAccessSilently) before or after a 401, and only surface
   needsCloudLogin when renewal genuinely cannot complete.

PRIVY_SESSION_COOKIE_VARIANTS also learns `privy-refresh-token` so a
renewal-only jar still counts as signed in rather than demanding an
interactive login while usable refresh material sits in the partition.

Tests: connection-config.test.ts covers the access/session split,
including the exact renewal-only cold-start jar from the issue repro.
2026-08-14 20:33:39 -07:00
hermes-seaeye[bot] 01512ca000 fmt(js): npm run fix on merge (#86631)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-15 03:31:07 +00:00
Teknium 5a3b593230 fix(desktop): order mid-turn user messages after the assistant output that predates them
A message typed while a turn streamed rendered ABOVE assistant output the
user had already watched arrive (#73793), and the retired
insert-before-the-active-reply fallback could splice the bubble mid-thread
— halfway up the chat — when the stream id was missing or stale (#83151).

Fix the class at every path that assigns a transcript position to a
mid-turn user message:

- New shared appendMidTurnUserMessage (rewind.ts): seal the live stream
  bubble in place (interim), append the correction at the live tail, and
  clear streamId so post-redirect deltas seed a fresh bubble BELOW the
  correction. Used by both the primary composer redirect path
  (use-prompt-actions) and the session-tile steer path
  (session-tile-actions), replacing the insert-before splice and its
  last-assistant mid-thread fallback.
- appendLiveSessionProjection now projects the resume/reload turn in
  arrival order (prompt → streamed output → correction → post-redirect
  output) instead of prompt → corrections → reply, so the projection
  agrees with the live transcript and messages no longer jump upward on
  reconnect. With the gateway's new correction_offsets the flat dump is
  split at each accepted-correction boundary; without offsets the
  corrections follow the projected reply.
- tui_gateway/server.py records correction_offsets (assistant text length
  at each accepted correction) on the inflight turn and carries them in
  _inflight_snapshot, only when complete, so resume can rebuild true
  arrival order. Older gateways/clients degrade cleanly.
- preserveLocalPendingTurnMessages and the projection's latest-user-run
  matcher now treat a live-tail assistant row between the prompt and its
  correction as part of the same turn's run, so arrival-ordered runs
  survive refreshes without dropping the prompt.

Fixes #73793. Fixes #83151.
2026-08-14 20:25:07 -07:00
Teknium 8064550025 chore: map contributor email for attribution audit 2026-08-14 20:24:51 -07:00
Teknium dac5f86313 fix(desktop): keep the session-list merge/dedup/order pipeline invariant-consistent
Completes the sidebar order/visibility class on top of the three salvaged
contributor commits:

- mergeSessionPage (#47203): interleave survivors against the
  title-preserving merged rows using the backend's effective-recency key
  (last_active with a started_at fallback), tie-preferring survivors so
  keep-set rows with no timestamps retain the old prepend contract.
- sidebar order helpers (#73314): dedupe live ids as well as persisted ids
  in reconcileFreshFirst/reconcileOrderIds/orderByIds so the shared-git-root
  flatMap path can neither render one repo once per project nor write the
  duplicates back into localStorage (the persisted feedback loop).
- Pinned section (#85969): resolvePinnedSessions falls back to the server
  `pinned` flag when the localStorage pin set is cold or clobbered, so a
  backend-pinned row is never simultaneously filtered out of every list and
  absent from the Pinned section (the "session vanishes entirely" state).
  session-pin-sync then adopts the pin locally on its next reconcile.

Regression tests cover survivor interleaving with optimistic bumps and
started_at fallback, duplicate live/persisted id dedup, and pin resolution
fallback (cold cache, lineage-root pins, undefined flag on old backends).
2026-08-14 20:24:51 -07:00
Jreevo b6d2f15b6c fix(desktop): dedupe persisted sidebar order ids to stop duplicate repo headers
The desktop sidebar persists repo/lane order in localStorage
(hermes.desktop.workspaceParentOrder / workspaceOrder). If that saved
list ever contains the same id twice, orderByIds() pushes the matching
item once per occurrence, rendering the same repo header twice inside a
project. reconcileFreshFirst() then preserves the duplicates, so the
corruption self-perpetuates across restarts and storage clears.

Verified on a live install: the persisted workspaceParentOrder contained
the same repo path at two positions, the backend project tree was clean,
and the duplicated header matched the duplicated id.

Treat persisted UI order as untrusted input: orderByIds() now skips ids
it has already emitted, and reconcileFreshFirst() dedupes the retained
tail so the next persist writes a clean list (self-healing).
2026-08-14 20:24:51 -07:00
james47 9247f4e1a8 fix(desktop): compare pinned/archived in the session list signature
`refreshSessions` swaps the session page into `$sessions` only when
`sameCronSignature` reports a change, and that signature compared row
content — id, lineage root, title, source, profile, preview,
message_count, last_active, ended_at — but not row state. A page whose
only delta was `pinned` was judged identical and discarded, so the row
cached in the atom kept its old flag indefinitely. An idle conversation
never moves any of the compared fields again, which is exactly the kind
a user goes and unpins.

`session-pin-sync` treats that row as authoritative. Its write guard
(daeedf67c) is released by a page that CONFIRMS the value it wrote, and
falls back to letting the server win once WRITE_GUARD_MS elapses with no
confirmation. Because the confirming page was filtered out one layer up,
the fallback was the only branch that ever ran: ~10s after an unpin the
next reconcile read the frozen `pinned: true` row and called
pinSession() again. Adoption marks the id `mirrored`, so the push pass
never corrected the backend either — the local pin set and
sessions.pinned drifted apart permanently, which is why four of five
pins rendered in the sidebar read pinned=0 in state.db.

Compare both flags so a pin-only page reaches the atom. That restores
the guard's confirm path and makes WRITE_GUARD_MS a backstop again
rather than the load-bearing branch. `archived` is included for the same
reason: it is row state a consumer reads. Neither flag moves outside a
deliberate user action, so the churn the gate exists to prevent is
unaffected.

The existing `releases the guard once a page confirms the written value`
test passes on main because it hands `$sessions` the confirming page
directly — the gap was in the pipeline that decides whether such a page
is ever delivered.

Fixes #76919

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 20:24:51 -07:00
liuhao1024 d029575407 fix(desktop): sort survivors by last_active in mergeSessionPage to prevent stale sidebar order
When multiple sessions are active/settled simultaneously, survivors
(sessions the server omitted from the fresh page) were prepended as a
block in their old relative order from the previous $sessions array.
This caused recently-interacted sessions to appear below older ones.

Now survivors are sorted by last_active descending and merged into the
incoming array at the correct position using a two-pointer merge,
so the sidebar always reflects true recency.

Fixes #47203
2026-08-14 20:24:51 -07:00
Teknium f8cc6d082e test(todo): make JSON-string coercion test order-agnostic
The type-coercion test pinned index order of todos, which #42649's
_normalize_order intentionally changes (in_progress lifts ahead of
earlier pending rows). Assert coercion by id instead of position.
2026-08-14 20:24:42 -07:00
Teknium 99033ab1f0 chore: map contributor email for PINKIIILQWQ 2026-08-14 20:24:42 -07:00
Teknium cf30d89586 fix(desktop): import MemoryRouter from react-router in collapsed-indicator test
The repo standardized on react-router (see find-bar.test.tsx); react-router-dom is not installed, so the salvaged test failed to transform.
2026-08-14 20:24:42 -07:00
Teknium 8bd83a9f7c test(gateway): pin get_update_result in metadata-mirror snapshot test
The test compares two _session_info snapshots taken at different times; the background update-check thread can complete between them and flip update_behind (None -> -1), making the equality assertion flaky once the suite runs long enough. Pin the value via monkeypatch.
2026-08-14 20:24:42 -07:00
LeonSGP43 0e7151ceae fix(todo): keep active step ahead of pending rows 2026-08-14 20:24:42 -07:00
Sora-bluesky 89fa829802 test(desktop): pin the tail-only contract for the loading and stall indicators
Regression coverage for #68634. The indicator family mounts only on the
thread's any-role tail (ba756333): a running bubble that is not the tail
stays silent, even when only a user or system row trails it, and the
optimistic-placeholder flow renders exactly one status row, the
placeholder's own.

Mutation-checked: relaxing the mount gate to a last-assistant walk fails
the two silence cases, and removing the gate fails all five.
2026-08-14 20:24:42 -07:00
Pink 6fcc202687 feat(desktop): show collapsed todo running indicator 2026-08-14 20:24:42 -07:00
Michael Huang 2cabeba563 fix(tui,desktop): refresh context usage live during active turns 2026-08-14 20:24:42 -07:00
Teknium 2b3c505463 chore: map salvaged contributor emails (audit_pr_attribution) 2026-08-14 20:24:32 -07:00
Teknium 73bcfddb3d fix(desktop): replay pending clarify prompts after reconnect
Clarify prompts share the same emitted-while-detached failure class the
pending-approval replay fixed: `clarify.request` rides `_block()`'s pending
registry, so a client whose transport was down when the event fired never
sees the question and the agent thread stays parked until timeout.

Widen the resume snapshot the same way:

- tui_gateway/server.py: `_live_session_payload` now carries
  `pending_clarify` — a read-only snapshot of the clarify prompt still
  blocking the session, scoped to the owning runtime sid. The registry stays
  authoritative; the embedded request_id resolves via clarify.respond.
- Desktop resume paths (`use-session-actions`) restore the parked clarify
  into the clarify store (multi_select preserved) and flag needsInput,
  mirroring restorePendingApproval on both the activate and resume paths.
- pending_approval replay now also forwards the queue-injected request_id so
  the restored prompt responds with exact-request correlation.
- Tests: server-side replay + scoping test; harmonized the #82087 replay
  test with the request_id `_ApprovalEntry` now injects.
2026-08-14 20:24:32 -07:00
Thomas Hudspith-Tatham 1134d2c998 fix(desktop): stage keyboard multi-select choices 2026-08-14 20:24:32 -07:00
Thomas Hudspith-Tatham 6cbe5a35b6 fix(desktop): support multi-select clarifications 2026-08-14 20:24:32 -07:00
Richard Howes 34d76a1df0 [verified] fix(desktop): reveal active clarify prompts
Reveal a blocking clarify card by re-arming the existing thread bottom-scroll bridge after the request row is hydrated. Keep background-session prompts isolated to their needs-input indicator.

Refs #53666.
2026-08-14 20:24:32 -07:00
konsisumer 38b9005b95 fix(desktop): replay pending approvals after reconnect 2026-08-14 20:24:32 -07:00
VooDoo Pixels f703e70618 fix: make desktop approval routing reliable
Correlate approval requests, reject stale responses, replay pending approvals after reconnect or session resume, and preserve fail-closed timeout behavior.
2026-08-14 20:24:32 -07:00
Teknium 62dbd87b1b chore: map contributor email for salvaged PR attribution 2026-08-14 20:24:15 -07:00
Teknium 898cc87125 fix(desktop): salvage session-race sequencing cluster — widen Stop cooldown to tile interrupts, prove submit ownership both ways
Follow-ups on top of the two salvaged commits:

- widen the #83855 recently-interrupted cooldown to the session-tile
  interrupt path (use-session-tile-delegate.interruptSession) — same race
  class, sibling call site: a tile Stop also clears busy before the
  gateway settles, so a quick tile edit/resend raced 4009 session busy.
  The recovered runtime id is marked too.
- regression test for the tile cooldown.
- refresh three #65328 ownership-proof assertions to tolerate the
  omit_messages flag main now sends on session.resume (toMatchObject).
- eslint import-order fix in utils.test.ts.
2026-08-14 20:24:15 -07:00
Simplicio, Wesley (ext) 33b39f3f4b fix(desktop): prove submit target belongs to selected session from both directions (#65328)
Fail closed on missing ownership cache entries and prove runtime ownership
forward+reverse against runtimeIdByStoredSessionIdRef before prompt.submit.
Thread the ownership cache through main wiring and session-tile submit.
Adds regression tests for forward mismatch, reverse-only proof, positive
map control, and cache-miss resume.

Closes #65328.
2026-08-14 20:24:15 -07:00
Olympusbuildz 801fd0b3d8 fix(desktop): interrupt-first after Stop so edit/resend avoids session-busy
Stop clears frontend busy immediately while the gateway may still wind
down. Edit/restore then passed interruptFirst=false and raced 4009
session busy. Keep a short per-session cooldown after cancel so rewind
still interrupt-first, and expire the submit-in-flight lock so a hung
submit cannot block the session forever.

Fixes #83855

Co-authored-by: Olympusbuildz <Olympus.roots@outlook.com>
Signed-off-by: Olympusbuildz <Olympus.roots@outlook.com>
2026-08-14 20:24:15 -07:00
Teknium 4ac12d53fb fix(tui_gateway): keep steer-mode fall-through bursts queued instead of interrupting
Desktop/TUI busy-input `steer` mode escalated any fall-through message
(steer() rejected, raised, or a non-steerable multimodal payload) into a
hard interrupt of the live turn. AIAgent.interrupt() also clears the
pending steer buffer, so a burst of user messages sent while the agent
was busy could be silently destroyed: earlier successfully-steered
messages were dropped from the buffer and the live turn was killed.

Steer-mode fall-throughs now keep pure queue semantics: preserved FIFO
in queued_prompt/queued_prompts and drained on turn end, per the
existing steer contract. Only explicit `interrupt` mode still fires
_interrupt_busy_session. No synthetic user messages are injected
mid-loop; accepted steers continue through the sanctioned OOB
steer-marker path.

Regression tests cover: rejected steer queues without interrupting,
steer exception falls back to queue, multimodal payload queues, a mixed
burst preserves accepted steers plus the queued fall-through, and a
fall-through burst drains all texts FIFO after turn end.

Fixes #86134
2026-08-14 20:24:06 -07:00
StanleyStetson 3995fd434c fix(tui_gateway): stop replaying live-turn user text after redirect
A mid-turn correction (Desktop session.redirect / busy-input interrupt redirect)
must not leave a server-queue self-copy of the live inflight user prompt.
Otherwise post-turn _drain_queued_prompt restarts that original text as a
fresh agent turn after Q completes (#84417).

Scrub text-only self-duplicates of inflight_turn.user on successful
redirect/steer, refuse admitting them in _enqueue_prompt, rewrite merged
"{P}\n\n{Q}" slots to Q-only, bump _queued_prompt_generation on compression
session rotation, and restore the claimed queue envelope when generation
cancels mid-drain. Stabilize profile-scoped agent-build unit tests under CI load.

Fixes #84417
2026-08-14 20:24:06 -07:00
Teknium b9482484f7 chore: map contributor email for attribution audit 2026-08-14 20:23:57 -07:00
Teknium 1c80085f04 fix(desktop): widen terminal-status handling to sibling sites
Two sibling sites still treated only 'completed'/'failed' as terminal:

- delegate-model.ts settled result rows as 'completed' for ANY status other
  than 'failed', so a delegate result row with status 'timeout' or 'error'
  (the statuses tools/delegate_tool.py actually emits on child timeout or
  crash) rendered behind a green check. Settled rows now map ok/completed
  to completed and everything else to failed.
- subagents.ts asStatus accepted a literal 'queued' payload status even on
  a subagent.complete event, leaving the row active forever. The fail-closed
  branch now runs before the queued fallback, so completion events always
  settle.
2026-08-14 20:23:57 -07:00
Michael Gannotti e3c3d0895d test(desktop): late progress events must not revive a timed-out subagent row
Folded-in coverage from PR #80045 (gannotti, #80018): after a terminal
subagent.complete, a stray late 'running' progress event must not restart
the spinner — the upsert guard keeps the settled failed status.
2026-08-14 20:23:57 -07:00
Sebastian Mause a01c7bb43b test(desktop): completion events with still-active payload statuses settle as failed
Folded-in coverage from PR #85995 (smause): a subagent.complete event whose
payload still says 'running' or 'queued' must settle the row as failed —
the completion event itself is the source of truth that the child is done.
2026-08-14 20:23:57 -07:00
David Metcalfe d3fee89993 fix(desktop): prefer synthesized timeout summary over stale progress text
Review feedback: prev?.summary could shadow the 'Timed out after Xs'
reason when a live event had populated it. timeoutSummary() now wins for
raw timeout status; add coverage for the missing-duration placeholder.
2026-08-14 20:23:57 -07:00
David Metcalfe 09b1726a1d fix(desktop): fail closed on unrecognized subagent.complete statuses; surface timeout reason
Follow-up to the #73728 normalization fix (supersedes the event-agnostic
fallback the maintainers flagged as incomplete):

- subagent.complete is terminal by definition — an unrecognized status on
  it now renders as 'failed' instead of falling through to 'running',
  which would recreate the immortal false-active row for any future
  backend status (the keep_open request on #73859).
- Live events keep the lenient 'running' fallback.
- Synthesize a 'Timed out after Xs' summary from duration_seconds when
  the backend completes with status 'timeout' and no summary, so the
  failed row explains itself.
- Tests: timeout reason synthesis + pruning, event-aware fail-closed vs
  lenient live fallback (13 total).
2026-08-14 20:23:57 -07:00
RelaxJonh a351c17d4a fix(desktop): normalise timeout/error subagent statuses to terminal (#73728)
The backend emits terminal statuses including 'timeout' and 'error' in
subagent.complete payloads, but asStatus() only recognised 'completed',
'failed', 'interrupted', and 'queued'. Unrecognised values fell through
to 'running', making timed-out subagents immortal in the active status
stack.

Fix: map timeout/error to 'failed', cancelled/canceled to 'interrupted'.
Nonterminal unknown statuses still default to 'running' for forward
compatibility.

Fixes #73728
2026-08-14 20:23:57 -07:00
Teknium 4faf12b9ce chore: map salvaged contributor emails (75day, KBANTH) 2026-08-14 20:23:47 -07:00
KBANTH f9f5c14f9a fix(desktop): keep live gateway across profile switches 2026-08-14 20:23:47 -07:00
Trevor Nash-Keller 077c04755e fix(desktop): sync active profile after reconnect 2026-08-14 20:23:47 -07:00
A9 8edb4626ca fix(desktop): refresh sessions on profile switch
Re-run the foreground session-list refresh whenever the active gateway profile changes, preventing rows from the previously selected profile from persisting in the sidebar.
2026-08-14 20:23:47 -07:00
Jakub Wolniewicz 21b57c61f4 fix(desktop): page remote profile session reads 2026-08-14 20:23:47 -07:00
Teknium 5a23ab513c chore: map salvage contributor emails to GitHub usernames 2026-08-14 20:23:18 -07:00
Guilherme Aguiar 01e542b764 fix(desktop): address transcript refresh review feedback 2026-08-14 20:23:18 -07:00
Guilherme Aguiar 1a2b0ca8cb fix(desktop): refresh active transcript on session changes 2026-08-14 20:23:18 -07:00
chelsealong f0748b451c fix(desktop): stop empty REST transcript refresh from wiping a warm resume
session.activate's persisted-transcript refresh reconciled unconditionally
against getLatestSessionMessages, so a transient empty REST page (e.g. a
backend respawn racing its own state.db read after a wake/reconnect) wiped
a transcript the activate response had just restored. Guard it the same way
the activate payload itself already is guarded a few lines above: an empty
authoritative page never overrides a non-empty cached transcript.
2026-08-14 20:23:18 -07:00