Commit graph

18108 commits

Author SHA1 Message Date
teknium1
c476ad35ae fix(update): refresh stale managed-uv catalog so SQLite runtime repair can succeed
The managed uv is installed with UV_UNMANAGED_INSTALL, which disables
'uv self update' by design — the swallowed failure left its embedded
python-build-standalone catalog frozen at bootstrap age forever.
python-build-standalone re-releases existing patch versions with fixed
SQLite (3.11.15 was re-cut with 3.53.1), so a stale catalog resolves
the same version number to the OLD vulnerable build, the probe rejects
it, and the patch-retry loop cannot recover because the fixed build
carries no newer number to try. Result: 'hermes update' printed a
guaranteed-failure provisioning warning on every run (issue #72093).

- When provisioning fails, re-bootstrap the Hermes-managed uv binary
  via the official installer (the only supported refresh for unmanaged
  installs) and retry provisioning once — only when the binary version
  actually changed, so no wasted download cycles.
- Never touch a caller-supplied uv outside the managed path.
- Soften the failure report from alarming ⚠ to informational ℹ and say
  why it is safe to wait: the WAL gate keeps databases out of WAL on
  vulnerable builds, and the next update retries.

Verified: 56 unit tests green; sabotage run (retry block removed) fails
the 3 new retry tests; live E2E replaced a fake managed uv via the real
astral installer and the refreshed binary resolved the 3.11 catalog.

Fixes #72093
2026-07-26 17:20:27 -07:00
brooklyn!
e2fbd0dcd7
Merge pull request #72303 from NousResearch/bb/desktop-session-status
fix(desktop): stop sidebar sessions from lying about whether they're running
2026-07-26 19:19:22 -05:00
Kevin Haddock
a75ec9278c fix(model): track explicit models: declarations in section 3 so a singular default_model doesn't suppress live discovery
A providers: entry with only a default_model/model (no explicit models:
list) is un-narrowed — the singular field is just the active selection.
Section 3 derived has_explicit_models from the merged models list, so
the lone default_model entry counted as an explicit catalog and
suppressed the /v1/models probe for no-key endpoints, leaving a
one-line /model picker menu for local llama.cpp/Ollama/vLLM servers.

Track explicit models: declarations separately at group-build time
(mirrors section 4's declaration-tracking from #40542 / PR #61928) and
gate the probe on that instead.

Salvaged from PR #68984 by @vigilancetech-com (the probe_custom_providers
gate removal in that PR is not taken — the GUI no-probe gate is
intentional).
2026-07-26 17:17:03 -07:00
teknium1
0f554ce19e test(gateway): drop source-reading guard test from #71671 salvage
Source-regex tests are banned (AGENTS.md 'Never read source code in
tests') — keep only the behavioral regression test.
2026-07-26 17:15:38 -07:00
hereicq
12fdaeaf1a test(gateway): trim docstrings 2026-07-26 17:15:38 -07:00
hereicq
3ec5fb076e fix(gateway): survive faulthandler.enable() when sys.stderr is None
faulthandler.enable() writes to sys.stderr by default, and raises
RuntimeError('sys.stderr is None') when the gateway is launched
without an attached console — e.g. via the Windows Startup VBS shim,
pythonw.exe, a detached service, or any parent that redirects stderr
to DEVNULL. Because this happens on the very first line of
GatewayRunner.start(), the whole gateway used to die at startup and
every configured platform adapter (Discord bot, Telegram, Slack, …)
would silently show offline until the user manually re-ran
'hermes gateway run --replace' from a real terminal.

Wrap the call and fall back to a log-file file descriptor
(logs/gateway_faulthandler.log) when stderr is unavailable, so
fatal-error stack dumps still land somewhere useful. If even the
fallback fails we log-and-continue rather than kill the gateway.

Repro traceback (from a real user's gateway-exit-diag.log, launched
via the Startup VBS with stdin_is_tty=false):

    File "gateway/run.py", line 7821, in start
        faulthandler.enable()
    RuntimeError: sys.stderr is None
2026-07-26 17:15:38 -07:00
teknium1
b792bd0529 feat(delegation): structured stall metadata + live per-child status in /agents
Completes #51690 on top of the salvaged #60378 timeout metadata:

- async_delegation: terminal 'stalled' events now carry structured
  stall context (stalled_after_quiet_seconds, stall_threshold_seconds,
  stall_phase idle|in_tool, stall_grace_seconds) on both single and
  batch paths, persisted in the durable row so restart-restored events
  keep it. Mirrors the sync path's timeout_seconds/timed_out_after_
  seconds/timeout_phase from #60378.
- list_async_delegations(): exposes seconds_since_progress and live
  children_activity (per-child api_calls, current_tool,
  seconds_since_activity) sampled from the dispatch's progress_fn
  outside the records lock; private monitor bookkeeping and callables
  never leak.
- /agents (CLI + gateway): background delegations render per-child
  activity rows, quiet-time hints, and the stalling state; gateway
  section is new (previously async delegations were invisible there).
  New locale key gateway.agents.background_delegations in all 17
  catalogs.

Tests: stall-metadata event shape, live-listing projection, gateway
/agents rendering (real registry dispatch, sabotage-verified), sync
timeout metadata fields, non-timeout None contract.
2026-07-26 17:13:52 -07:00
mahdiwafy
8e163852d8 fix(delegate): include explicit timeout metadata in subagent results
Add timeout_seconds, timed_out_after_seconds, and timeout_phase to timeout
results so parent agents and users can distinguish timeouts before the
first LLM call from timeouts after one or more API calls.

Also attach diagnostic_path to the N>0 API-call timeout error message,
matching the existing zero-API-call timeout path.

Addresses part of #51690 and #17308.
2026-07-26 17:13:52 -07:00
brooklyn!
2b38d5ad59
Merge pull request #72308 from NousResearch/bb/tool-group-bounding
fix(desktop): stop reads and edits vetoing tool-call grouping
2026-07-26 19:13:22 -05:00
Brooklyn Nicholson
19d84d1071 test(desktop): pin the sidebar spinner's liveness contract
Investigating the missing-spinner report turned up no defect in the seeding
path: the active_list poll already lights a row for a turn the renderer never
saw start, holds it across polls, and follows a recycled runtime id onto its
new stored session. Pin all three so the reap change can't silently regress
turn-start while fixing turn-end.

Two boundaries worth naming rather than rediscovering:

- `starting` is deliberately NOT working. It means agent_build_started without
  agent_ready, and _start_agent_build runs on any incidental RPC that needs the
  agent — not just a prompt — so treating it as a turn would spin the row on
  merely opening a session.
- $workingSessionIds is keyed by STORED id and drops entries whose
  storedSessionId is null, while message.start flips busy without carrying one.
  A runtime that was never seeded with a stored id therefore goes busy
  invisibly. That is the remaining path by which a spinner can go missing.
2026-07-26 19:02:46 -05:00
teknium1
136f8dab67 refactor(gateway): promote autonomous silence matcher to shared response_filters helper
Follow-up to the salvaged #71756: instead of webhook importing cron's
private _is_cron_silence_response, the loose autonomous-lane matcher now
lives in gateway/response_filters.py as is_autonomous_silence_response,
sharing LIVE_GATEWAY_SILENT_MARKERS with the interactive exact-marker
rule so the marker sets can never drift. Cron and webhook both delegate
to it. Interactive gateway behavior unchanged.
2026-07-26 16:55:42 -07:00
Gumclaw
55d3272286 fix(webhook): honor [SILENT] when the agent explains its own silence
A webhook route that answered `[SILENT]` still delivered, whenever the model
added a sentence saying why it was staying quiet:

    [SILENT]

    The new inbound was the same email quoted back a second time, on a ticket
    we already answered. Nothing new to reply to, so I closed it.

Webhook subscription prompts tell the agent to answer `[SILENT]` on a tick that
produced no story — a duplicate inbound, a stand-down because a sibling lane
already replied, a routine close. Nobody is waiting on the other end of a
webhook, so a "nothing happened" message has no reader.

Delivery went through the live gateway's `is_intentional_silence_response`,
which requires the response to be EXACTLY a marker. That rule is right for an
interactive chat: swallowing a real answer because it opens with a marker is
much worse than showing a stray marker. It is the wrong trade for an autonomous
lane, where a leaked non-story is a pointless notification on every tick and
models reliably append the explanation that flips the check back to "deliver".
Cron already resolved this the other way — `cron/scheduler.py` treats a marker
on its own first or last line as silence — so the two autonomous lanes
disagreed while the interactive path was fine.

Suppress in `WebhookAdapter.send`, before the deliver-type switch, so every
route (log, github_comment, cross-platform) behaves the same. Reuses cron's
`_is_cron_silence_response` rather than restating the rule, so the two lanes
cannot drift; prose that merely mentions a marker mid-sentence still delivers.
The interactive gateway path is untouched.

Tests: six cases in tests/gateway/test_webhook_adapter.py — bare marker,
marker + trailing prose (the reported shape), marker on the last line, a real
report, a report quoting a marker mid-sentence, and a `log` route. Verified
red-first: with the suppression removed the three silence cases fail
("Expected send to not have been awaited") while the three delivery cases still
pass, so the tests assert the fix rather than the framework.
2026-07-26 16:55:42 -07:00
Brooklyn Nicholson
058aa34d8a fix(desktop): stop reads and edits vetoing tool-call grouping
A run of 3+ adjacent tool calls collapses into the `.tool-group-scroll`
window, but `shouldBoundToolGroup` took `hasUnboundable` as a run-level
veto: a single exempt row anywhere in the range disabled collapsing for
the entire run. The exempt set was clarify, image_generate, execute_code,
read_file and every file-edit tool — which is most of what a coding
session does, so in practice runs never collapsed. Replaying a real
session's transcript: 84 consecutive calls, 30 of them veto-triggering,
zero windows.

The code-body entries were never needed. Everything ToolEntry renders
carries `data-tool-row`, and the `:has([data-tool-row][data-tool-open])`
rule already lifts the cap and the mask. A diff row mounts open, so it
frees the group the moment it appears; a collapsed row is a one-line
status whose body is not in the DOM at all, so there is nothing to clip.
Collapsing the row drops the group back to a compact window, which the
JS veto could not do.

Narrow the opt-out to the two components that bypass ToolEntry and so
can never emit `data-tool-row`: clarify and image_generate.
2026-07-26 18:49:05 -05:00
Brooklyn Nicholson
6b273f419a fix(desktop): keep thinking traces and tool calls across a mid-turn switch
preserveReasoningParts was gated on exact text equality with the cached row.
Mid-turn the authoritative text has advanced by a delta or two, so the guard
fails and the row is rebuilt from the gateway's inflight projection — which
is text-only. The renderer's cache is the sole carrier of a running turn's
structure, so switching away and back stripped the reasoning and every tool
call, leaving the turn looking inert.

Carry tool calls alongside reasoning, dedupe them on toolCallId, and match on
same-turn (identical text, or authoritative text extending the cached text)
rather than strict equality. Attachment refs and image re-appending stay on
the strict path: those reconcile a settled row, and a growing row is by
definition not settled.
2026-07-26 18:39:11 -05:00
Brooklyn Nicholson
42a30d13db fix(desktop): settle sessions that vanish from the live snapshot
session.active_list is authoritative about absence, but the renderer only
read the rows it returned. A turn that ends while the websocket is degraded
— a remote gateway on a flaky link, a reconnect, a profile swap — drops out
of the gateway's _sessions without Desktop ever seeing the running=false
edge, so the row spins forever and the busy->idle transition that paints the
green unread dot never fires.

Track live runtime ids per gateway profile and settle anything that
disappears between polls through publishSessionState so the real transition
fires. Profile scoping is load-bearing: background profiles are served by
other gateways and never appear in this profile's snapshot, so an unscoped
reap would dark out every other profile's running rows.
2026-07-26 18:39:03 -05:00
brooklyn!
bd6437d605
Merge pull request #72288 from NousResearch/bb/composer-undo
Give the composer its own undo stack
2026-07-26 18:38:11 -05:00
brooklyn!
2ec84c5d25
Merge pull request #72245 from NousResearch/bb/desktop-idle-churn
perf(desktop): stop the whole transcript re-rendering on sash drag
2026-07-26 18:36:00 -05:00
teknium1
759f68bc25 fix(dashboard): stop rendering 'unknown' model placeholders in session lists
Sessions that die before title generation (or predate model tracking)
rendered as 'Untitled · unknown · 0 msgs' — two placeholders stacked in
one row reads as breakage to Hermes Cloud users. Now:

- Session rows omit the model segment entirely when the store has no
  model (no more 'unknown' + dangling separator).
- The Overview 'Recent Sessions' card falls back to the message preview
  as the row label (italic, same treatment as the History list) instead
  of a bare 'Untitled', and skips the duplicate preview paragraph when
  the preview IS the label.
2026-07-26 16:30:28 -07:00
erick713006
2df57fe641 perf(web): lazy-load dashboard routes and split heavy vendors
The production dashboard build packed almost every page plus xterm/three/
plot into one large JS chunk, which trips Vite's 500kB warning and slows
first paint even when the user only opens Sessions/Config.

- Lazy-load route pages in App.tsx behind Suspense
- Defer mounting the persistent embedded chat host (and xterm) until the
  first /chat visit, while keeping the sticky PTY latch afterward
- Add rolldown vendor codeSplitting groups (react, xterm, three, plot,
  motion, ui) and raise chunkSizeWarningLimit modestly to 600kB

Addresses #25912 (partial: route lazy-load + vendor splits + fallbacks;
not yet CI bundle analysis or documented entry budget).

Verified locally: npm run typecheck, npm run test (97), npm run build
with separate page/vendor chunks.
2026-07-26 16:30:28 -07:00
HexLab98
aff48958d3 fix(deepseek): drop retired models from picker and provider defaults
Stop offering deepseek-chat/reasoner in the static catalog and point
fallback/aux defaults at the permanent v4 IDs. Keep retired aliases in
a detection-only map so /model deepseek-chat still resolves to deepseek.
2026-07-26 16:28:41 -07:00
HexLab98
cc7c418b33 fix(deepseek): remap retired chat/reasoner aliases to v4-flash
DeepSeek cut off deepseek-chat and deepseek-reasoner on 2026-07-24.
Sending those IDs now returns HTTP 400; rewrite them (and fuzzy
reasoner names) to deepseek-v4-flash so saved configs keep working.
2026-07-26 16:28:41 -07:00
trymhaak
148497f6d6 fix(kanban): strip stale session routing from dispatched worker env
A long-lived gateway can have platform routing (HERMES_SESSION_* /
HERMES_CRON_AUTO_DELIVER_*) mirrored in os.environ from a previous turn.
_default_spawn() copied that process environment verbatim into detached
kanban workers, so a worker calling kanban_create treated the inherited
chat/topic as its origin and auto-subscribed the child task — the task's
terminal notification then woke an unrelated chat.

Strip every registered session-context routing key from the worker env
unconditionally (the dispatcher is detached from every conversation);
board, workspace, task, branch, profile, model, and credential
propagation are unchanged.

Salvaged from PR #69181 (both commits squashed; the PR's second commit
fixed the first's engagement-latch assumption).
2026-07-26 16:27:52 -07:00
Salim
c93ed07459 fix(kanban): route active named profile through the active adapter map
A gateway running under a named active profile (e.g. `hermes -p main gateway`)
stamps kanban auto-subscriptions with notifier_profile=main, but
_authorization_adapter() treated any name other than the literal "default"
as a multiplex secondary and consulted only _profile_adapters — empty on
standalone gateway-per-profile deployments. The helper failed closed, the
notifier rewound the claim, and the notification was silently retried
forever (#71340).

Recognize the gateway's own active profile name as primary so its stamped
subscriptions resolve via self.adapters; genuinely secondary profiles keep
the fail-closed lookup.

Salvaged from PR #62380 (the unrelated blocked-reason truncation change is
intentionally not taken).
2026-07-26 16:27:52 -07:00
srojk34
dda6f0f63e fix(kanban): widen notifier pre-filter to secondary-profile platforms
_collect()'s active_platforms pre-filter was derived solely from
self.adapters (the default profile), so a subscription owned by a
secondary profile on a platform the default profile never connected
(e.g. beta owns discord, default has no discord adapter at all) was
skipped before claim_unseen_events_for_sub ever ran. Unlike the
disconnected-adapter path, an unclaimed event is never rewound, so this
was a permanent, silent notification/wake loss — directly contradicting
the point of routing notifications via the owning profile
(c69643026/b225b30d0). Same cross-profile-adapter-lookup bug class the
delivery-side _authorization_adapter chokepoint already guards against,
one gate earlier. The precise per-profile check still runs unchanged at
delivery time, with its existing rewind-on-None safety net.
2026-07-26 16:27:52 -07:00
tymrtn
67826e3068 fix: scope kanban auto-subscriptions to active profile 2026-07-26 16:27:52 -07:00
Brooklyn Nicholson
92f62bedd7 fix(desktop): extend the composer undo stack to the message edit composer
The edit composer pastes through the same `insertComposerContentsAtCaret` the
main composer uses, so it had the identical bug: the Range-based insert never
reaches Chromium's undo stack and Cmd+Z skipped past the paste to destroy the
edit before it. Its inline-ref and trigger-chip inserts mutate the DOM directly
too, with the same result.

Both surfaces now share `useComposerUndo`. The hook already keys its
document-level `beforeinput` claim off `document.activeElement`, so the two
mounted instances stay independent — only the focused editor's stack responds.
Undo/redo is handled ahead of Escape here, since a stray Cmd+Z falling through
would cancel the whole edit rather than step back one change.

`insertRefStrings` banks through `withUndoPoint` rather than recording after the
insert, which would have snapshotted the state it was meant to restore.
2026-07-26 18:21:50 -05:00
Brooklyn Nicholson
bc2ddf5bab fix(desktop): give the composer its own undo stack so Cmd+Z sees a paste
The rich editor mutates its DOM through `Range` rather than the browser's
editing commands, because `execCommand('insertText')` is ~O(n²) on large
multiline blobs and froze the composer for seconds on a big paste (#45812).
Those mutations never reach Chromium's undo stack, so a paste was invisible to
it — Cmd+Z skipped straight past the pasted text and undid whatever edit came
before it, leaving the paste stranded and the earlier edit destroyed.

Owning the stack outright is the only coherent fix; a half-owned one interleaves
our snapshots with Chromium's own typing entries and undoes them out of order.
Every edit path now banks its pre-edit state, and the editor claims Cmd+Z /
Cmd+Shift+Z (plus Ctrl+Y) instead of letting the native command run. Snapshots
are plain text + a caret offset rather than DOM, since the editor already
round-trips losslessly through composerPlainText/renderComposerContents.

Consecutive keystrokes coalesce inside a 600ms window, so undo steps back by a
typing burst the way a native editor does rather than one character at a time.
Electron's Edit menu `{ role: 'undo' }` fires the native command without a
keystroke the renderer can see, so a capture-phase `beforeinput` listener claims
historyUndo/historyRedo too and keeps the menu item and the shortcut in
agreement. Switching drafts resets the history — undoing into another
conversation's text is worse than having none.

Co-authored-by: David Metcalfe <80915+DavidMetcalfe@users.noreply.github.com>
2026-07-26 18:21:35 -05:00
teknium1
f34a69b1cd test(tui): prove agent build installs the selected profile's secret scope
Sabotage-verified: fails when the set_secret_scope call in
_start_agent_build is removed.
2026-07-26 16:19:52 -07:00
kyssta-exe
2db6b8c85b fix(tui_gateway): scope secrets and MCP discovery to the active profile (#67605)
The dashboard/desktop profile switch was partial — switching to profile X
ran the launch profile's resources in two ways:

1. MCP discovery was gated on the launch profile's config having
   mcp_servers. If the launch profile had none, the background thread
   never started and zero MCP servers existed for every profile. Fix:
   always start discovery and let discover_mcp_tools() handle the
   empty-config case.

2. The profile secret scope (.env credentials) was never installed on the
   tui_gateway path. get_secret() fell through to os.environ, resolving
   secrets from the launch profile instead of the selected one. Fix:
   install set_secret_scope(build_profile_secret_scope(...)) alongside
   every set_hermes_home_override() call site:
   - compute_host.py:_ensure_server_session (build-time)
   - server.py:_build (lazy resume)
   - server.py:_handle_resume_session (_make_agent scope)
   - server.py:_handle_resume_session (_init_session scope)
   - server.py:_handle_submit_or_edit (per-turn handler)
2026-07-26 16:19:52 -07:00
Gille
b93fd077c0 fix(session-search): allow scrolling compacted lineage history 2026-07-26 16:18:31 -07:00
teknium1
5f5afb1eef fix(delegation): count streamed tokens as liveness in the stale monitor
Include last_activity_ts in the progress token sampled from each child.
_touch_activity ticks on every streamed chunk ('receiving stream
response'), every tool transition, and API-call start/completion — so a
child mid-stream on a long response is alive even though api_call_count
only advances when the call completes. Same liveness signal as the
compaction inactivity budget (PR #71508): if tokens are flowing it never
dies; staleness is measured from the last streamed token / tool activity
/ API call.
2026-07-26 16:15:29 -07:00
teknium1
99a381f310 fix(delegation): progress-based stale detection for detached async runners
Replace the wall-clock timeout watchdog (from #60234) with progress-based
staleness detection, on by default with zero config:

- The async registry now accepts a progress_fn per dispatch; delegate_task
  wires a sampler over the batch's child agents (api_call_count +
  current_tool from get_activity_summary()).
- A single monitor thread sweeps running delegations: a child whose
  progress token keeps advancing is never touched, no matter how long it
  runs. A frozen token past the stale threshold (450s idle / 1200s
  in-tool, mirroring the sync-path heartbeat monitor) marks the record
  'stalling' and interrupts the child.
- A stalling child that unwinds within the grace window (120s) finalizes
  through the NORMAL path, preserving its partial results. One that never
  returns is force-finalized with a terminal 'stalled' completion event so
  the owning session hears an outcome and the async slot frees.
- Late runner returns after force-finalization are deduped by the
  begin/push/finish finalization split (kept from #60234).

Why not a timeout: delegation.child_timeout_seconds defaults to 0 by
deliberate design (DEFAULT_CHILD_TIMEOUT rationale) — a timeout-based
watchdog never arms for default configs, leaving the reported silent-
profile symptom (#60203) unfixed, and when armed it kills legitimately
slow heavy subagents mid-task. Progress detection distinguishes 'wedged
at first API call' from 'grinding through a 2h review'.

Builds on izumi0uu's finalization-atomicity work from #60234.
2026-07-26 16:15:29 -07:00
izumi0uu
65420cdecd fix(delegation): timeout stuck async child runners
Async background delegation can leave gateway sessions holding only a dispatched handle when the detached runner wedges before it can return and enqueue a completion. Enforce the configured child timeout in the async registry so the parent observes a terminal timeout event and the async slot is released.

Constraint: Issue #60203 reports long-lived gateway processes with background child delegates that never produce completion events despite child_timeout_seconds being configured.

Rejected: Relying only on _run_single_child timeout handling | it cannot finalize the async registry when the outer runner thread itself never reaches normal completion.

Confidence: high

Scope-risk: narrow

Directive: Keep background delegation completion owned by the async registry whenever detached workers can outlive the caller's immediate control.

Tested: .venv/bin/python -m pytest tests/tools/test_async_delegation.py tests/tools/test_delegate_subagent_timeout_diagnostic.py tests/tools/test_delegate.py -q

Tested: .venv/bin/python -m ruff check tools/async_delegation.py tools/delegate_tool.py tests/tools/test_async_delegation.py

Tested: git diff --check

Not-tested: Multi-day real gateway degradation; covered with deterministic stuck-runner registry tests.
2026-07-26 16:15:29 -07:00
teknium1
c593f7face chore(contributors): map salvaged-PR author emails for #59278/#62712/#63001 2026-07-26 16:14:15 -07:00
teknium1
a0bc7d5572 fix(kanban): snap notify-sub cursor to current MAX(task_events.id) at creation
Fixes the boot-storm half of issue #29905: kanban_notify_subs.last_event_id
defaulted to 0, so a subscription created on an already-active task replayed
the task's ENTIRE terminal-event backlog on the next notifier tick. With
many stale subs (27 observed in the report) a gateway boot after downtime
burst 100+ notifications in one go.

add_notify_sub now snaps the cursor to COALESCE(MAX(task_events.id), 0) for
the task inside the same INSERT, so new subscriptions start caught up and
only receive events that occur AFTER subscribing. The gateway slash-command
and kanban-tool auto-subscribe paths run at task creation, where the
snapshot is just the 'created' event — behavior there is unchanged.

Stale fixtures that asserted the literal 0 creation cursor now assert
'cursor unchanged/unclaimed' instead, which is what they actually meant.
2026-07-26 16:14:15 -07:00
teknium1
a945364ba2 test(gateway): harden notifier isolation regression + block_loop_detected e2e coverage
Follow-ups from review of salvaged PRs #59278 and #62712:

* test_kanban_notifier_isolates_per_subscription_failure previously
  created the good subscription first; list_notify_subs() has no
  ORDER BY, so the good delivery happened before the bad claim raised
  and the test passed even without the isolation fix. The bad task is
  now created first AND a deterministic-order shim forces the failing
  subscription to be iterated first, so the test fails on the old
  whole-tick-abort behavior.

* New test_notifier_delivers_block_loop_detected_triage_ping: drives a
  block_loop_detected event through one notifier tick end-to-end,
  asserting the triage ping reaches the adapter and the cursor advances
  (the sweeper review of #62712 flagged that only DB-level emission was
  tested).
2026-07-26 16:14:15 -07:00
David Beyer
0b632f772a fix(gateway): zero-sub early exit for kanban notifier board polling
Salvaged from PR #63001 (reduced scope): probe each board with the new
read-only kanban_db.count_notify_subs() before the writable connect(),
so boards with zero subscriptions are never opened writable on the 5s
notifier tick (no schema migration, no WAL/-shm sidecar churn, no
checkpoints).

The PR's machine-global .notifier.lock singleton gate was deliberately
NOT salvaged: a lock-winning default-profile gateway cannot deliver a
secondary profile's subscriptions in standalone-profile deployments
(profile routing fails closed in _authorization_adapter), so the lock
could suppress delivery entirely. The probe captures the per-tick cost
win without that regression.
2026-07-26 16:14:15 -07:00
rhylryan21
4436eacebf fix(gateway): kanban notifier delivery reliability
- honor SendResult(success=False) instead of discarding it, so an adapter
  that REPORTS (not raises) a soft send failure — e.g. the Telegram adapter's
  "Not connected" mid-reconnect — no longer advances the cursor past an
  undelivered event and silently loses the notification. Addresses the
  notifier half of #31901.
- add block_loop_detected to the notifier's TERMINAL_KINDS so a task routed to
  triage for a human decision (re-blocked past the recurrence limit) actually
  pings its subscribers instead of stalling silently.
- raise MAX_SEND_FAILURES 3 -> 12 (~60s at the 5s tick) so a transient
  Telegram/API outage does not permanently unsubscribe a live channel now that
  reported soft-failures also reach this counter.
- route active-profile-stamped subscriptions via the primary adapter on a
  single-profile gateway (self.adapters[platform] when the stamped
  notifier_profile equals the active profile). Related to #56802.

Adds test_kanban_notifier_rewinds_claim_on_reported_send_failure asserting a
reported send failure leaves the event unseen (rewound) rather than consumed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-26 16:14:15 -07:00
AlexFucuson9
f6ccfa6bd3 fix(gateway): isolate per-subscription failures in kanban notifier
The kanban notifier _collect() loop iterates subscriptions without
per-subscription error handling. When claim_unseen_events_for_sub raises
for one subscription (e.g. DB corruption, lock contention), the entire
tick aborts — silently blocking delivery for ALL other subscriptions.

Wrap the per-subscription logic in try/except so one bad subscription
logs a warning and continues to the next, instead of jamming the
entire notifier.

Closes #59269
2026-07-26 16:14:15 -07:00
yuexiong
83dc0b9b85 fix(install): clear stale Windows cua lock 2026-07-26 16:01:54 -07:00
yuexiong
1a6754efa3 chore: map contributor email 2026-07-26 16:01:54 -07:00
yuexiong
23d0dca832 fix(install): reap Windows cua installer process tree 2026-07-26 16:01:54 -07:00
teknium1
26670bce95 fix(gateway): dedupe chat_type wiring after composing #58615 with #60769
Both the wake chat-scope salvage (#72191, merged) and the DM-topic
metadata salvage added HERMES_SESSION_CHAT_TYPE plumbing; the rebase
auto-merge kept both copies. Dedupe the ContextVar declaration, _VAR_MAP
entry, set_session_vars parameter/token, and the run.py call-site kwarg,
and prefer the persisted chat_type column with delivery_metadata as the
legacy fallback in the notifier wake path.
2026-07-26 16:01:32 -07:00
embwl0x
f174a08c93 fix(gateway): let internal events bypass topic lobby 2026-07-26 16:01:32 -07:00
embwl0x
1bdec6f065 fix(kanban): preserve telegram dm topic metadata 2026-07-26 16:01:32 -07:00
Brooklyn Nicholson
0226d1162e Revert "perf(desktop): mount tooltips lazily"
Reverts the tooltip half of 4798994dc; keeps the idle-cost scenario.

Lazily mounting Radix on first hover measured well (105k -> 26k
TooltipProvider renders per drag) but broke 18 tests across 12 files.
Those tests are not incidental: the repo has an established convention of
asserting `[data-slot="tooltip-trigger"]` at mount to prove a control
carries a tooltip, and deferring the mount invalidates all of them at
once. There is also a real behavior risk the convention was protecting —
`asChild` puts the slot on the button element itself, so arming REPLACES
the node, which is exactly the kind of identity change that breaks focus
restoration and ref-holding call sites.

A 4x cut in tooltip churn is not worth reworking every tooltip assertion
in the app plus taking that risk, on a component with ~107 call sites.
If it's worth revisiting, the right shape is probably making
TooltipProvider itself cheap (one app-level provider) rather than
deferring the mount per call site — that preserves the DOM contract these
tests encode.

The genuine win in this branch stands on its own: the $layoutTree
subscription fix (commits 83 -> 12 on a sash drag) is unaffected.
2026-07-26 18:00:02 -05:00
Brooklyn Nicholson
8896fc7500 fix(desktop): stop a chip inserted after a word from swallowing its space
`plainTextInRange` serialized the caret's preceding content through a bare
<div>, but `composerPlainText` appends "\n" to any block element that isn't the
editor slot. So `beforeText` always looked like it ended in whitespace and the
separating space was never inserted — dragging a file in after a word produced
`review@file:...` glued together.

Marking the scratch container with RICH_INPUT_SLOT makes it serialize in the
same coordinates as the editor. Same fix lands in the new `caretOffsetInEditor`,
which measures caret offsets the same way.
2026-07-26 17:50:18 -05:00
brooklyn!
48bdde1deb
Merge pull request #72230 from NousResearch/bb/bootstrap-marker-triage
fix: make the bootstrap-complete marker consistent across every install path
2026-07-26 17:48:57 -05:00
teknium1
6d1e08b2bc fix(dashboard): QA pass — log colors, nameless channels, config bool, UX gaps
Companion fixes from a full dashboard QA pass (every page dogfooded
live), on top of the cherry-picked #31863 header-slot fix:

- ChatPage: harden the header-slot effect further — useLayoutEffect and
  never write the slot while inactive, so the handoff commentary and
  ownership rule live next to the code.
- LogsPage: level classification used raw substring matching, so INFO
  lines carrying 'parse_errors=0' (or paths like errors.log) rendered
  red. New unit-tested classifier (web/src/lib/log-classify.ts) anchors
  on the hermes_logging level token with a word-boundary fallback.
- Channels API: plugin platforms (irc, ntfy, photon, teams, …) rendered
  as nameless title-cased cards ('Irc', 'Ntfy') with empty descriptions.
  Two root causes: (1) plugin discovery never ran in the dashboard
  server process, so plugin_entries() was empty; (2) Platform enum
  pseudo-members claimed plugin ids before the registry could attach
  labels. The catalog now discovers plugins explicitly and resolves
  plugin metadata first; added descriptions + docs links for bundled
  plugin platforms and the msgraph_webhook / whatsapp_cloud / relay
  enum members. Regression test sabotage-verified against the old
  enum-first ordering.
- Config schema: updates.refresh_cua_driver declared type 'bool'
  (schema vocabulary is 'boolean'), so the switch rendered as a text
  input holding 'true'.
- Page titles: '/mcp' rendered as 'Mcp' via the naive capitalize
  fallback; literal-label table now covers MCP/Files/Channels/Webhooks/
  Pairing/System (unit-tested).
- AuthWidget: skip the guaranteed-401 /api/auth/me probe in loopback
  mode — every dashboard load logged a console error for nothing.
- Model picker: with no filter, providers that actually have models
  float above the wall of '0 models' rows.
- Cron: empty state now carries an actionable Create button.
2026-07-26 15:17:30 -07:00
02356abc
8c3c52b008 fix(dashboard): stop ChatPage from clearing all pages' header action buttons
When embedded chat is enabled, ChatPage renders persistently outside
<Routes> but is initially hidden during the plugin-loading window
(~2-4s). Once plugins finish loading, ChatPage mounts for the first time.

Its header-slot effect had early-return branches for !isActive and !narrow
that actively called setEnd(null). Because the user was on /cron, /models,
/sessions, or any non-chat page, this wiped the action buttons that the
current page had already placed in the header.

Affected pages include:
- Cron page — CREATE button disappears
- Models page — 7D/30D/90D filter buttons disappear
- Sessions page — search box disappears

The fix collapses the two early-return branches into one and removes the
setEnd(null) calls. Now ChatPage only sets end when it actually owns the
slot (isActive && narrow), and lets the normal cleanup handle unmounting.

PageHeaderProvider already clears all slots on pathname change via
useLayoutEffect, so ChatPage's active clearing was redundant and harmful.

Fixes #31862

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-26 15:17:30 -07:00