Commit Graph

22526 Commits

Author SHA1 Message Date
Teknium eeb2d23b86 fix(memory): accept new_text as an alias for content (#86642)
memory replace/remove target an entry by old_text but supply the replacement
via content — an asymmetric pairing. Callers naturally reach for new_text to
mirror old_text (it's exactly the patch tool's old_string/new_string shape),
which left content empty and errored 'content is required'. The failure also
rendered tersely, making the cause easy to miss and costing a retry.

Accept new_text as an alias for content on both shapes:
- single-op: coalesce content = content or new_text in memory_tool() + a
  new_text param wired through the registry handler.
- batch ops: content = op.content or op.new_text (and in the approval-gate
  preview builder).
- schema: document the alias on content and add a new_text property to the
  single-op params and batch item props so strict validators accept it.
content wins if both are set.

Tests: new_text alias on single add/replace, batch add/replace, and
content-wins-over-new_text. 43/43 across memory tool + schema suites.

Co-authored-by: Teknium <teknium1@users.noreply.github.com>
2026-08-14 21:15:08 -07:00
Teknium 77248f8cb0 fix(desktop): clear the stale stream target on the assistant-tail append path too
Sibling site of the idle-resume rule from the stale-fold fix: the
assistant-tail append exit (user row persisted, no projection row) still
carried the journal's streamId onto a not-running resume, which kept the
journal entry alive (persistInFlightTurnState only clears when streamId is
null) and re-folded the same tail on every open. Apply the same
keepPending gate and pin it with a regression assertion.

Refs #85308
2026-08-14 21:10:13 -07:00
Nicolas Formenton 9184915c42 fix(desktop): skip stale journal duplicate folds
The inflight-turn journal can outlive the turn it recorded (reclaim,
reconnect or restart races skip the settle that clears it). On session
resume the fold then re-appends journaled assistant rows to a transcript
that already holds the committed replies, so the conversation ends with
duplicate answers in scrambled order. The fold also carried the stale
entry's streamId onto the resumed state on an idle resume, which kept the
journal entry alive (persistInFlightTurnState only clears when streamId is
null) and re-folded the same tail on every open.

Detect text-level staleness before the append path: when every recoverable
journaled assistant row already exists as committed text in the base
transcript, treat the entry as caught up and clear it. Only keep a stream
target when the resumed session is genuinely running (keepPending), so an
idle resume self-heals instead of re-folding.
2026-08-14 21:10:13 -07:00
Eugeniusz Gilewski 4d4bf0e0e5 test(desktop): spy on the active localStorage implementation (#82832)
The inflight journal regression test always spied on Storage.prototype,
but Node 26's jsdom setup can provide a plain in-memory localStorage fallback.
That left the test unable to observe the setItem call in CI even though the
per-session journal write was correct.

Select the native window.Storage prototype when available and otherwise spy
on the active localStorage object, preserving the assertion across both
storage implementations.

Refs #82832
2026-08-14 21:10:13 -07:00
Eugeniusz Gilewski 4588126bbd fix(desktop): bound inflight journal persistence (#63047)
The desktop journal synchronously read, parsed, cloned, and rewrote one
aggregate localStorage value while streamed turns were repainting. Large
tool results and multi-session state could therefore block the renderer and
leave the app unresponsive, while the existing macOS diagnostic path lacked
a real native hide/restore regression check.

Store bounded recovery projections under per-session keys, migrate legacy v1
data once, isolate quota and storage failures, and preserve the newest
recoverable tail without allowing oversized writes to replace valid state.
Add a real Electron/CDP macOS-arm64 A/B harness with native visibility control,
renderer heartbeat and Settings/composer/transcript checks, plus focused
regressions. Keep bulk tool payloads, diagnostics, and the existing recovery
merge behavior out of the hot path.

Fixes #63047
2026-08-14 21:10:13 -07:00
Teknium 2f094a2645 test(desktop): align transcript paging expectations with include_compacted reads 2026-08-14 21:08:14 -07:00
Kinkoolino-Hermes e4e7dd5c6e fix(desktop): preserve full transcripts for running sessions 2026-08-14 21:08:14 -07:00
zuowen7 bbb6cc7e99 test: cover tool-message dedupe and latest paging for include_compacted (#80680)
Dedupe key now includes tool_call_id/tool_calls/tool_name: compaction
copies carry those fields verbatim, so identical tool messages across
generations still collapse, while distinct tool calls sharing
role/content/timestamp are never merged. Add endpoint-level coverage
for the desktop's real read path (limit + order=latest +
include_compacted=true).
2026-08-14 21:08:14 -07:00
zuowen7 f71f91a39b fix(desktop): surface compaction-archived messages in transcript reads (#80680) 2026-08-14 21:08:14 -07:00
spfcraze 5f619cfa0e fix(gateway): don't restart supervised services on clean exit
The s6 finish script for profile-gateway services restarted on ANY exit
except EX_CONFIG (78) — including clean exit 0. Restart-on-normal-exit
turns an intentional stop into a reconnect loop: the ashriel-discord
storm in #76435 made 1,000+ connections and got the bot token reset by
Discord.

The finish script now exits 125 (permanent failure, no restart) for both
clean exit 0 and EX_CONFIG; only non-zero, non-78 exits (genuine crashes)
restart normally.

Scope note: #76435 bundles a second, separable symptom (Windows desktop
updater showing the literal 'managed outside dashboard' sentinel). Its
root cause is undiagnosed and #22733 covers the dialog-explanation path;
this PR is the gateway half only.

Tests: behavioral — the rendered finish script is executed via sh for
exit codes 0/78/1/137, asserting no-restart for clean stop and fatal
config, restart for crashes. Pre-existing EX_CONFIG test still passes.
2026-08-14 21:07:21 -07:00
David Metcalfe ebb2859132 fix(clarify): preserve resolved answers when clear_session races a button response
Session-boundary cleanup (gateway/run.py: run-finalization and prompt
delivery-failure paths) calls clear_session to cancel pending clarifies.
It unconditionally overwrote every entry's response with the empty
cancellation sentinel, even entries already resolved by a button callback
or text intercept. A waiter that had already observed the resolved event
would then return the empty sentinel instead of the real answer, silently
discarding the user's response on /new, gateway shutdown, or cached-agent
eviction.

First-writer-wins: clear_session now cancels only entries whose event is
not yet set; already-resolved entries keep their response. Mirrors the
guard resolve_gateway_clarify gained for the same contract. Reported by
doryani-ai on PR #75732; regression test covers the button-then-cleanup
interleaving.
2026-08-14 20:58:42 -07:00
ygd58 525ca9ca1c fix(gateway): match the profile-namespaced session key in the clarify bypass lookup
Fixes #82975.

The adapter-level clarify reply bypass in gateway/platforms/base.py's
handle_message() built its session_key via build_session_key(...)
without a profile= argument, defaulting to the legacy agent:main
namespace. The runner registers pending clarifies under
SessionStore._generate_session_key()'s key, which DOES include
profile=self._resolve_profile_for_key(source). Under a named-profile
multiplex these diverge, so the bypass lookup at
clarify_gateway.get_pending_for_session(session_key, ...) misses --
the user's answer to a pending clarify() gets routed to the adapter's
busy-session queue instead of resolving it. The turn then hangs until
the clarify's 3600s timeout, with no inbound message: log line and no
"Gateway intercepted clarify text response" log line, matching the
reported Telegram symptom exactly.

Verified the divergence directly: _resolve_profile_for_key() returns
None when multiplex_profiles is off (default) -- byte-identical to
the prior implicit profile=None, so this only changes behavior for
multiplexed deployments, matching the issue's exact reported scope.

Fixed by using the same self._session_store._resolve_profile_for_key()
the runner's key generator calls, guarded with getattr() + a None
fallback since _session_store is set via a setter and can be unset
for adapters that never call set_session_store() -- preserving prior
behavior for any such adapter rather than introducing a new crash.

Added a regression test alongside the existing bypass coverage: with a
mocked session_store configured for profile multiplexing, a clarify
registered under the profile-namespaced key must still be found and
resolved (not routed to the busy queue). Verified as a genuine
regression by reverting the fix and confirming the new test fails
with the exact reported symptom (the message handler never gets
awaited -- the clarify lookup misses).

21/21 pass across the five directly related clarify test files;
16/16 across the broader multiplex/clarify-progress test files (no
regression).
2026-08-14 20:57:37 -07:00
HexLab98 39f3bb9312 test(compression): cover degenerate compress_end same-session handoff keep
Regression for #83248: handoff beyond compress_end must not clear a valid
same-session _previous_summary via the cross-session discard path.
2026-08-14 20:52:16 -07:00
HexLab98 fc100f4b3b fix(compression): scan full window for handoffs before cross-session discard
A degenerate compress_end can hide an in-window handoff past the cut; the
#57835 guard then cleared a valid same-session _previous_summary (#83248).
2026-08-14 20:52:16 -07:00
Teknium bc5805c35f fix: compare base-URL hostnames, not substrings, in provider-identity checks
Port of the bug class from earendil-works/pi#7933 (DeepSeek base-URL
detection matched by raw substring, missing case variants and matching
lookalike URLs). Hermes had the same class at five sites:

- cli_agent_setup_mixin.py: keyless-custom-endpoint detection treated any
  URL containing the OpenRouter host substring (path segment, lookalike
  domain) as OpenRouter, and missed case variants of the real host.
- models.py validate_requested_model: same substring check for routing an
  openrouter provider with a custom base_url to the custom catalog.
- runtime_provider.py: local-endpoint autodetect matched the string
  localhost anywhere in the URL, including remote hostnames containing it.
- gateway/run.py: /status endpoint display, same local-host substring.
- agent_runtime_helpers.py: Nous Portal cache-layout detection matched
  the nousresearch substring anywhere in the URL.

All sites now use the existing base_url_host_matches / base_url_hostname
helpers (exact host or subdomain, case-insensitive). Regression tests
proven to fail against the old predicates.
2026-08-14 20:47:13 -07:00
Evgenii f9d64b9a9d fix(cron): add reliable trigger feedback in Web and Desktop clients
- Shared per-job trigger controller (apps/shared) coalesces duplicate
  clicks for the same profile+job inside a mounted client while letting
  unrelated jobs run independently; the backend durable claim remains
  authoritative across windows/processes.
- Two-phase feedback everywhere: the action stays disabled/spinning
  while the request is in flight and the terminal success/error is
  reported once, after the HTTP response — no premature success toast
  (Web), matching the Desktop info notification.
- Desktop keeps the 24h trigger timeout for the synchronous long
  operation and fences stale profile/list responses and unmounted
  surfaces; the sidebar trigger button shows a spinner while busy.
2026-08-14 20:46:50 -07:00
Evgenii 1a8625abee fix(cron): harden gateway fire admission and provider compatibility
- The gateway api_server fire webhook acknowledges 202 only after a
  durable claim + execution row exist (admission failure stays retryable
  as 503; a live claim answers 200 duplicate), then dispatches the
  claimed snapshot with the live runner adapters (delivery parity with
  the built-in ticker, including relay-fronted and E2EE platforms).
- Legacy single-phase providers (a documented fire_due override without
  split hooks) keep being driven through their own hook. Capability
  detection now credits claim_fire AND fire_claimed overrides, so
  Chronos is correctly classified split-aware (its re-arm lives in
  fire_claimed; the redundant fire_due passthrough override is removed).
- Multi-profile dashboards fail closed for external providers: an
  unscoped reconcile would disarm other profiles' armed one-shots in the
  shared NAS registry.
- Manual runs (cronjob run) carry the owner-bearing claimed snapshot
  through every entry point, composing with upstream's manual-run
  heartbeat (#76502) and background dispatch.

Note: current main moved the dashboard NAS webhook to a pure
forward-to-gateway design (the gateway owns execution and live
adapters), so the dashboard-side claim/tracking machinery from earlier
revisions of this PR is dropped; the durable admission contract lives in
the gateway webhook path.
2026-08-14 20:46:50 -07:00
Evgenii acaafcc6bb fix(cron): make immediate execution race-safe
- claim_job_for_fire returns the atomically claimed snapshot with a unique
  fire owner; heartbeat_fire_claim renews the lease; mark_job_run fences
  terminal writes by expected_fire_owner so a stale worker cannot record
  over a replacement claim.
- run_one_job heartbeats the fire claim and forwards a combined cancel
  event (ownership loss OR external cancel) into run_job; the agent path
  is interrupted cooperatively and script-based jobs (no_agent + pre-run
  scripts) are hard-stopped with a process-tree kill (POSIX killpg
  SIGTERM then SIGKILL for surviving group members; Windows
  taskkill /T /F), with a bounded pipe drain so a SIGTERM-ignoring
  descendant cannot wedge the worker on communicate() EOF.
- Shutdown interruption is scoped to the exact execution token instead of
  the bare job ID, so a replacement run of the same job never consumes a
  stale interrupted flag.
- fire_claim_fence serializes save/deliver side effects per profile+job
  with a cross-process flock; remove_job prunes the fence-lock entry.
- Preserves upstream BaseException terminal recording (#73973),
  completed one-shot retention (#80624), blocked_config preflight
  (T1-26), and the advance_next_runs batch on top of current main.
2026-08-14 20:46:50 -07:00
Teknium 5d9e4aaaf2 fix: raise ProviderStreamError for choiceless error chunks + regression tests 2026-08-14 20:46:42 -07:00
AlexFucuson9 48cca664c6 fix: detect in-stream error chunks in SSE streaming path
Some OpenAI-compatible providers (DeepInfra, etc.) return validation
errors as in-stream SSE chunks: HTTP 200 with choices=None and
error_type/error_message in model_extra. The streaming loop silently
dropped these chunks, causing EmptyStreamError ("empty stream") and
pointless retries on the same bad request.

Fix: check for error_type/error_message on chunks with no choices
before skipping. When found, raise RuntimeError with the provider's
error message so the error classifier can properly handle it.

Fixes #65631
2026-08-14 20:46:42 -07:00
hermes-seaeye[bot] efc3cd8697 fmt(js): npm run fix on merge (#86634)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-15 03:42:49 +00:00
Teknium 662a77f31e fix(desktop): don't let a stale resume snapshot mark an open chat idle
Fixes the #70449 symptom that remained after the two salvaged commits:
opening or viewing a chat whose turn is still running cleared its working
indicator. `session.activate` / `session.resume` report `running` as a
snapshot taken when the RPC was issued; a turn that started or kept
streaming while the RPC was in flight has already marked the runtime busy
in the live cache, and both resume paths overwrote that newer truth with
the stale `running: false`, dropping the session out of the working set
and painting it done mid-turn.

Add `resolveResumedBusy`: a snapshot saying running always wins (adopting
a live turn is never stale), but a snapshot saying idle can no longer
rewind a live busy — the turn's own terminal signal (running:false via
session.info / the settle path) stays the only authority that ends it,
and the background-sync reaper still clears truly lost turns. Wired into
both the warm `session.activate` path and the cold `session.resume` path,
reading the freshest cache entry rather than the pre-await state.

Includes an eslint --fix formatting pass over the touched files.
2026-08-14 20:40:24 -07:00
Teknium cb3ca0af0e chore: add contributor email mapping for razultull 2026-08-14 20:40:24 -07:00
razultull 6202fca059 fix(desktop): keep session marked in-progress during async delegation
Salvaged from #51358 (razultull), rebuilt against the rewritten status
architecture on main. The original PR patched setSessionWorking /
noteSessionActivity / the statusbar counter, all of which have since been
replaced (busy now lives in session-states.ts, the watchdog only marks
stalled, and the statusbar Agents item already shows a pure subagent count
— the conflated counter the PR split no longer exists).

What still applied is the core bug: a parent that delegates via
delegate_task(background=true) ends its own turn the moment the handle
returns, so the sidebar row dropped to a plain idle dot while the spawned
subagents kept working for minutes — the session read as "done" mid-task.

Add $delegatingSessionIds — sessions whose subagents are still queued or
running — as an input to the session dot projection, claiming the same
'background' treatment as running background processes (and yielding to
'working' while the parent turn itself is live). Uses the same
runtime→stored bridge, lineage aliasing, and fresh-chat runtime-id
fallback as $backgroundRunningSessionIds, and clears by construction the
moment the last subagent reaches a terminal status.
2026-08-14 20:40:24 -07:00
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