Commit Graph

22191 Commits

Author SHA1 Message Date
kshitij dafdba324a feat(models): per-model metadata overrides via model_overrides config
Add a unified model_overrides config section that lets users manually
declare context_window, max_output_tokens, capabilities, cost, and
family for any provider+model — winning over models.dev, OpenRouter, and
hardcoded defaults.

Resolution order (first hit wins):
  1. model_overrides.<provider>.<model_id>  (per-provider+model)
  2. model_overrides.<provider>._default    (per-provider default)
  3. model_overrides._default               (global default)
  4. Normal catalog resolution

Key subtlety: an unknown model id (not in the
catalog) derives base metadata from sensible defaults before patching,
so overriding a model the catalog doesn't know yet is the supported
self-unblock path. This is exactly the #84482 scenario (Upstage
solar-pro4/syn-pro wrong context) and the #8731 scenario (custom/local
models with manual capability declaration).

Wired into:
  - get_model_capabilities() — patches capability fields; unknown models
    get safe defaults (tools on, vision/reasoning off) before patching
  - lookup_models_dev_context() — context_window override, checked before
    catalog lookup so it works even for providers not in PROVIDER_TO_MODELS_DEV
  - get_model_info() — merges override dict onto catalog entry (shallow
    merge); for unknown models, the override is the sole source of metadata
  - get_model_context_length() — step 0b in the resolution pipeline,
    before custom_providers (0c) and before any network probe

Config example:
  model_overrides:
    upstage:
      solar-pro4:
        context_window: 524288
      syn-pro:
        context_window: 65536
    custom:my-local-vllm:
      my-llava-model:
        context_window: 8192
        supports_vision: true
        supports_reasoning: false
        supports_tools: true
    _default:
      context_window: 128000

Fixes #8731
Fixes #84482
Refs #47247
2026-08-14 02:03:54 +05:30
kshitij cdea8214cb docs: fix stale parity claim in _BARE_BILLING_PROVIDERS comment
The set is no longer in parity with agent_init's fail-fast gate (which
still skips openrouter for a different reason: default route, not
unroutable). Say so instead of claiming parity.
2026-08-14 02:02:18 +05:30
liuhao1024 1c87772186 fix(tui_gateway): restore openrouter provider on session resume
BARE_BILLING_PROVIDERS incorrectly included "openrouter" alongside
"auto" and "custom".  OpenRouter is a fully routable provider with
its own API key and base_url — sessions that used OpenRouter store
billing_provider="openrouter", and dropping it forces resume to the
current global model (e.g. a custom endpoint), which is the wrong
provider for the stored model.

Remove "openrouter" from the bare-bucket set so OpenRouter sessions
correctly restore their provider identity on resume.

Fixes #57588
2026-08-14 02:02:18 +05:30
Teknium a364390dab feat: forward repeat through cron.manage add (#85602)
cronjob(action=create) has supported a repeat cap since the tool
existed, but the ws handler dropped the param — UIs building on
cron.manage could not create run-N-times jobs. Forward it (digit
strings accepted, None keeps schedule-kind defaults). One-shot
relative schedules (bare 30m/2h) already flow through the schedule
string untouched.
2026-08-13 13:14:57 -07:00
Teknium bfff32ae8c chore: map contributor email for @sjungwon03 (PR #79787 salvage) 2026-08-13 12:31:02 -07:00
Teknium d0be93bd9a fix: explicit fallback api_mode always wins; clean up dead-code guard
Maintainer fixup on the #79787 salvage:

- An explicit fb.api_mode of "chat_completions" was silently overridden
  by the codex_responses / bedrock re-detection pass (which only skipped
  re-detection when the pre-computed mode was non-default). Track
  explicitness in fb_api_mode_explicit and gate the whole re-detection
  block on it.
- Replace the locals().get('fb_api_mode') dead-code hack with clean code
  (fb_api_mode is always bound at that point).
- Restore the post-resolve /anthropic + api.anthropic.com host check for
  named custom providers whose base_url comes from config rather than
  the fallback entry (#32243, #49247), which the PR's restructure dropped.
- Add regression tests: explicit api_mode honored (incl. explicit
  chat_completions not overridden), /anthropic-hint fallback detected
  pre-rewrite, api_mode forwarded to resolve_provider_client, plain
  fallback unchanged.
2026-08-13 12:31:02 -07:00
sjungwon03 17a675574c fix: preserve anthropic_messages api_mode during fallback activation
Fallback activation determined api_mode from the POST-rewrite client
base_url, losing the Anthropic wire signal for /anthropic endpoints
routed through provider 'custom', and never honored an explicit
fb.api_mode config field. Pre-compute fb_api_mode from the ORIGINAL
fallback base_url hint (before _to_openai_base_url rewriting), honor
the explicit api_mode config field, check provider name before the
base_url gate, and pass api_mode into resolve_provider_client at the
fallback call site.

Salvaged from PR #79787 (chat_completion_helpers.py hunks; the
auxiliary_client.py hunk is redundant with #85466's wrap_base fix).
2026-08-13 12:31:02 -07:00
Xie ddbef9cd79 fix(auxiliary): keep ZAI Coding Plan routing 2026-08-13 12:30:29 -07:00
kshitij a0939901df fix: address review feedback from #85512
- /model switch now refreshes agent._custom_providers from the config
  loaded during the switch before re-evaluating cache policy — a
  prompt_caching flag added to config.yaml after session start was
  invisible to a mid-session switch (policy read the stale init-time
  snapshot while context_length resolution used the live list).
- Production-path test: real config.yaml in the modern providers: dict
  shape through the real loader chain, exercising the init-order fallback
  (no _custom_providers attr) for both the fable opt-in and the opus
  explicit opt-out.
- Pin operator kill-switch precedence: _cache_disabled (prompt_caching.
  cache_ttl falsy) beats an explicit per-model prompt_caching: true.
2026-08-14 00:59:38 +05:30
kshitij 4fa728b6be fix: follow-up polish for salvaged PR #85512
- Log (debug) instead of silently swallowing capability-lookup failures in
  anthropic_prompt_cache_policy — a swallowed failure would otherwise
  downgrade an explicit prompt_caching: true to (False, False) with zero
  trace. Matches the sibling MoA branch's logger.debug style.
- Use load_config_readonly() for the None-fallback in
  get_custom_provider_model_capability: the helper only reads, and the
  fallback fires on the blank-stub paths (agent init before
  _custom_providers is assigned, MoA/auxiliary destination planning), so
  skip the ~135us defensive deepcopy per call.
- Add route-isolation regression tests at both levels (config helper +
  agent policy): a prompt_caching declaration for one provider route must
  never apply to another route with the same model name. Mutation-checked:
  both tests fail when the URL match is disabled.
2026-08-14 00:59:38 +05:30
fangliquanflq 316f31c9d5 fix(agent): honor prompt caching capabilities for aliases 2026-08-14 00:59:38 +05:30
Brooklyn Nicholson e505ff9777 feat(desktop): add reset-to-defaults to the statusbar context menu
Once you have toggled a few items on and off there is no way to get
back to the shipped layout short of remembering which ids are in
STATUSBAR_HIDDEN_BY_DEFAULT. Add a row to the bar's right-click menu
that restores that set.

The row is disabled rather than hidden when nothing is customized, so
it also advertises that a shipped layout exists. Reset touches item
layout only — whole-bar visibility is a separate preference, and
resetting from the bar's own menu should not make the bar you are
right-clicking disappear.
2026-08-13 14:29:18 -05:00
Teknium c692312704 feat(kanban): GC stale done-task notify subscriptions
Now that subscriptions survive `done` (completion is reversible —
on every 5s notifier tick forever. Add
kanban_db.purge_stale_done_notify_subs(): one DELETE removing subs
whose task has been done with no new events past a retention window
(age = latest task event, falling back to completed_at/created_at, so
any activity exempts the task; a reopened task is exempt by status
alone). The notifier watcher runs it per board once at startup and at
most hourly, re-reading kanban.done_sub_retention_days (config.yaml,
default 30; 0 disables) at each sweep.
2026-08-13 12:21:04 -07:00
Dmytro Afanasiev b640e63035 fix(kanban): preserve TUI subscription after done 2026-08-13 12:21:04 -07:00
Dmytro Afanasiev 294272c18c fix(kanban): preserve notifier subscription after done 2026-08-13 12:21:04 -07:00
Teknium dcbfc79f8d fix: forward include_disabled through cron.manage list (#85566)
cronjob(action=list) defaults to include_disabled=False, so pausing a
job makes it vanish from any ws-driven UI's next refresh — an
enable/disable toggle reads as silent deletion (reported against the
Hermes-Bot-Mode routines pane). Forward the flag from params.
2026-08-13 12:14:33 -07:00
kyinhub e5fd1c7b43 fix(kanban): make creator wake turns graph-safe
Carry the worker's completion handoff into the synthetic creator wake
turn and label it as an automatic notification with inspect-the-board /
don't-recreate guidance, so a woken orchestrator doesn't re-decompose
work that already exists (#70752).

Salvaged from PR #71100 by @yinkev; ported onto the restructured wake
region (delivery_mode gating, scope_id, sub chat_id destinations). The
auto_subscribe_on_create config-default half of the original PR was
dropped as already superseded on main.
2026-08-13 12:10:34 -07:00
Teknium 056f10acf6 fix(gateway): goal/heartbeat manager lookups for internal events skip activity touch
Widen #62804's class fix: _get_goal_manager_for_event and
_get_heartbeat_manager_for_event also call get_or_create_session on
behalf of the triggering event; when that event is internal the lookup
must not advance the user-activity clock either.
2026-08-13 12:04:35 -07:00
embwl0x 77166a5c46 test(gateway): cover activity-touch call contract 2026-08-13 12:04:35 -07:00
embwl0x 5462f689ba fix(gateway): keep internal wakes from extending sessions 2026-08-13 12:04:35 -07:00
Teknium f57116b231 fix(gateway): run wake-only kanban delivery before cursor advance with rewind/retry
For a push-adapter subscription with delivery_mode='wake' the visible text
ping is intentionally skipped (the send_passive gate), so the wake injection
IS the sole delivery — yet the event cursor advanced BEFORE the wake, which
then ran best-effort with its failure swallowed. A single failed wake
permanently lost the event.

Apply the same ordering the non-push (api_server) self-post branch already
uses: attempt the wake BEFORE advancing the cursor; on failure rewind the
claim (_kanban_rewind) and bump the per-sub failure counter so the next tick
retries; on success reset the counter; drop the subscription after
MAX_SEND_FAILURES consecutive failures like text sends do. notify+wake mode
is unchanged: the text ping is the delivery and the wake stays best-effort
after the cursor advance.

Extracts the residual delivery-ordering insight from closed PR #84191.

Co-authored-by: MaximCrabbe <crabbemaxim@gmail.com>
2026-08-13 12:04:07 -07:00
Brooklyn Nicholson 120e465c97 fix(desktop): show statusbar by default
The whole-bar visibility atom defaulted to false (opt-in). Flip it to
true so the bar shows on first launch. The context-usage meter and
other diagnostic items remain hidden via STATUSBAR_HIDDEN_BY_DEFAULT,
so only the core status items (gateway health, model pill, command
center) appear out of the box. The toggle keybind and ⌘K row still
let users hide it.
2026-08-13 14:01:49 -05:00
Teknium 4ef7ee9971 chore(contributors): map xaviersudre email for PR #75040 salvage 2026-08-13 11:56:43 -07:00
xaviersudre 0b600c859a fix(kanban): wake API subscriptions in destination session 2026-08-13 11:56:43 -07:00
Teknium 720f0443a0 chore: release v0.20.1 (2026.8.13) 2026-08-13 11:52:47 -07:00
Teknium 991e2efdf4 chore: map contributor emails for cherry-picked commits 2026-08-13 11:49:24 -07:00
Teknium 541fad3c3c test(cron): reconcile summarizer tests with honest chain wording and composed no_agent gate
The cherry-picked tests predate #85508's honest fallback-chain phrasing
and each other: assertions pinned the old 'exhausted or unavailable'
literal and #83188's no_agent fallback-note behavior, which #77648's
mode gate supersedes (no provider classification at all for no_agent
jobs). Assert the composed contract instead.
2026-08-13 11:49:24 -07:00
cation98 efbbc993f6 fix(cron): avoid false provider failure summaries 2026-08-13 11:49:24 -07:00
Botsson e5573a8f8c fix(config): recognize cron script timeout 2026-08-13 11:49:24 -07:00
Jody Bagdonas 7d039d7ee6 fix(cron): classify script timeouts separately 2026-08-13 11:49:24 -07:00
Yanir-R d1fc20432f fix(cron): don't attribute no_agent script failures to a provider
`_summarize_cron_failure_for_delivery` classifies a failed job by
substring-matching the error prose — "timed out", "429",
`authenticat|authoriz` — and maps any hit onto a provider-shaped
explanation, without consulting the job's execution mode.

A `no_agent` job IS its script: `run_job` short-circuits it before any
model is reached. Provider timeouts, rate limits, auth errors and
fallback chains are therefore structurally impossible for it, yet those
branches are tested first.

`_run_job_script` reports a timeout as "Script timed out after {n}s:
{path}". That contains "timed out", so a shell script exceeding its
timeout is delivered to chat as:

  ⚠️ Cron 'x' failed: provider timeout. Fallback chain was exhausted
    or unavailable.

for a job that never opened a socket, sending the reader to inspect
model routing while the actual fault is a shell script. "429" or
"authentication" appearing anywhere in a script's output misfires the
same way.

Gate the three provider branches on `not job.get("no_agent")` and let
script jobs fall through to the existing generic cleaner, which already
reports the real error and names the script. No new message text.

The auth branch carries a word-boundary guard so "oauth" and "4015" do
not trip it, which addresses one substring false-positive; gating on
mode removes the remaining class for script jobs.

Tests: the summarizer had no direct coverage — the only test referencing
it patches it out and asserts on its arguments. Adds parametrized cases
pinning both directions: script jobs are never blamed on a provider
(including when their output contains "429" or "authentication"), and
agent-mode jobs keep the existing provider summaries unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 11:49:24 -07:00
Teknium 2e3895224a test: update _ProviderCollector construction for new name arg
Sibling test pinned the old zero-arg constructor; salvaged #80493 gave
_ProviderCollector a required provider name (used for skill registration
and PluginContext delegation).
2026-08-13 11:49:14 -07:00
Teknium e70b187d51 chore: map salvage contributor emails 2026-08-13 11:49:14 -07:00
Greg Gibeau c600fd46bd fix(memory): complete discovery and registration parity for out-of-tree providers
Builds on the three salvaged commits: adds the sources and integration points
they leave out, so a pip-installed memory provider is not a second-class
citizen next to a directory install.

Discovery
- Project-local providers (./.hermes/plugins/<name>/), gated on
  HERMES_ENABLE_PROJECT_PLUGINS exactly as PluginManager gates its own project
  scan. Completes the four sources CONTRIBUTING.md and AGENTS.md already
  promised; memory was the only discovery system missing two of them.
- find_provider_dir() now resolves a package entry point to its directory.
  This is load-bearing: config_schema.py (the dashboard panel) and cli.py (the
  `hermes <provider>` subcommands) are read from disk rather than imported, so
  without a directory a pip-installed provider silently lost both.
- list_memory_provider_names() includes entry-point providers, so they appear
  in the dashboard's memory.provider dropdown.

Resolution stays import-free. hermes_cli.plugins.resolve_module_origin() is
extracted from _resolve_module_source() (added by the salvaged #76567) and
shared, so discovery walks a module's file layout instead of importing it.
find_provider_dir() is called from the dashboard and from argparse setup, long
before the operator has chosen a provider — importing every installed candidate
would execute third-party code on the strength of a package being present.
A test asserts the resolution leaves no side effects and no sys.modules entry.

Registration
- PluginContext gains register_memory_provider(). Memory was the only provider
  category without one; context engine, image gen, video gen, web search,
  browser, TTS, transcription, secret source, dashboard auth and platform all
  have one.
- _ProviderCollector delegates unknown register_* calls to a real
  PluginContext instead of carrying three hand-written no-ops. It silently
  dropped register_tool/register_hook, and had no register_auxiliary_task at
  all — despite PluginContext.register_auxiliary_task documenting a memory
  provider (hindsight's pre-retain dedup) as its worked example. It can no
  longer drift behind PluginContext.
- A raise after register_memory_provider() no longer costs the provider. The
  loader caught it into a debug log, discarded the registered instance, and
  fell through to "instantiate any MemoryProvider subclass" — returning a
  different, unconfigured provider. A silent downgrade that looked like
  success, and the exact outcome of calling register_auxiliary_task.

Activation is unchanged: still gated on memory.provider naming the plugin, and
covered by a test so the real PluginContext cannot start requiring
plugins.enabled — that would break every existing user-installed provider.

Verified end to end against a real third-party provider (kainappsinc/elephant)
installed by pip alone, with no directory copy: it appears in the dropdown,
resolves its directory, loads with its tools, and renders its dashboard panel.

Closes #40101.
2026-08-13 11:49:14 -07:00
Mike Smith a883977b12 test(plugins): activation-contract coverage for entry-point classification
Documents and tests the routing contract the sweeper review asked about:
classification records the manifest but does not activate anything.

- model-provider test now exercises providers.get_provider_profile() against
  the pip-only name (None today — providers discovery is directory-based)
  and asserts the module never leaks into sys.modules via that path.
- new test for the mnemosyne shape: a pip entry point duplicating a
  same-name directory provider. The pip copy is classified exclusive and
  never imported; the directory copy still activates through
  plugins.memory discovery, exactly once.
- _classify_entrypoint_kind docstring now states the activation contract
  explicitly: pip-only providers were equally unactivatable pre-change
  (both destination systems are directory-only; the
  hermes_agent.memory_providers entry-point group has no consumers), so
  classification only removes the wasted import. Entry-point activation
  is tracked upstream (#40644 for memory); this change is its
  prerequisite, preventing double import once it lands.
2026-08-13 11:49:14 -07:00
Mike Smith 450bd0930a fix(plugins): never import parent packages of dotted entry points
find_spec() on a dotted module name imports the parent package first,
executing its __init__.py — which is exactly where a provider's heavy
imports typically live (fastembed -> onnxruntime and friends). The
previous classifier only preserved the no-import property for
top-level entry points.

_resolve_module_source() now resolves only the top-level name with
find_spec() (import-free for top-level names) and walks the remaining
dotted segments through submodule_search_locations by hand, mirroring
PathFinder's file conventions (part.py module / part/__init__.py
package). Namespace packages, zipped modules, extension modules, and
anything else unexpected fall back to standalone (the safe default).
.pyc origins map back to source via source_from_cache.

Regression: a dotted entry point whose parent __init__.py writes an
execution marker and imports the child — asserts the parent never
executed and neither module enters sys.modules during classification.
Fails against the previous implementation (marker written), passes now.
2026-08-13 11:49:14 -07:00
Mike Smith 826e9d18af fix(plugins): classify pip entry-point provider plugins without importing
Entry-point (pip-installed) plugins exposing register_memory_provider()
or register_provider() + ProviderProfile were treated as plain
standalone plugins and eagerly imported by the general PluginManager,
even though memory and model providers have their own discovery
systems and the module has no register() for the general manager to
call. The import registered nothing and paid the module's full import
cost in every Hermes process (a pip memory provider pulls fastembed ->
onnxruntime, ~60 MB RSS).

Entry-point manifests now get the same source-scan classification as
directory plugins via a shared _detect_kind_from_source() helper: the
module is resolved with importlib.util.find_spec (no import) and its
first 8192 chars are scanned for provider markers. Memory providers ->
kind=exclusive, model providers -> kind=model-provider; both are
recorded for introspection and skipped by the general loader.
Unresolvable or non-Python modules stay standalone (default behavior
unchanged).

Tests: an enabled pip entry-point memory provider is never imported;
a pip entry-point model provider routes to providers/ discovery.
2026-08-13 11:49:14 -07:00
Simone Marzola 364adc89af fix: support packaged memory provider skills 2026-08-13 11:49:14 -07:00
Teknium 74ce299070 test(agent): align explicit-base auxiliary tests with host-anchored rewrite policy
Unknown hosts (e.g. gateway.example.com) no longer get /anthropic→/v1;
use a real dual-surface MiniMax base for the rewrite assertions and add
a case proving Anthropic-only gateways keep their path on the OpenAI wire.
2026-08-13 11:45:27 -07:00
Teknium 6f33f510e8 fix(agent): anchor dual-surface marker matching to the URL host
Substring matching over the whole URL let a path containing
'api.minimax' false-positive an Anthropic-only gateway into the
/anthropic→/v1 rewrite. Parse the host and match exact-domain /
subdomain suffixes (plus the api.minimax.* prefix family) instead.
2026-08-13 11:45:27 -07:00
686f6c61 0d24c4f413 fix(agent): only rewrite /anthropic→/v1 for dual-surface hosts
Unconditionally rewriting any */anthropic base_url to /v1 broke
Anthropic-only custom gateways (e.g. Alibaba Bailian Token Plan) used by
auxiliary compression/vision under provider=auto. Keep MiniMax dual-surface
hosts rewriting; leave pure Anthropic paths intact.

Fixes #83642
2026-08-13 11:45:27 -07:00
brooklyn! 266b2b3611 fix(update): repair failed Node deps on an already-current checkout (#85539)
A failed npm install during `hermes update` prints "Fix npm and re-run
`hermes update`" -- but re-running on a current checkout hit the
"Already up to date!" early return before the Node refresh, so the
repair advice could never work and node_modules stayed stale forever
(#77211).

The commit_count == 0 path now runs the Node refresh through
_repair_node_deps_on_current_checkout. _update_node_dependencies
self-gates on the lockfile hash, which is only recorded after a
SUCCESSFUL npm install (and re-trips when node_modules is missing or
the web toolchain never landed), so healthy installs pay one hash
check and nothing else; a previously failed install actually repairs.
A clean refresh pairs with the web build like every other call site;
a failed one surfaces the fix-npm hint instead of "Already up to
date!".

Fixes #77211.

Co-authored-by: RelaxJonh <RelaxJonh@users.noreply.github.com>
Co-authored-by: JonthanaHanh <JonthanaHanh@users.noreply.github.com>
2026-08-13 18:42:42 +00:00
iso2kx cd344a280f fix(auxiliary): honor /anthropic-suffixed gateway base_url on aux + fallback calls
`_try_anthropic()` applies the configured `model.base_url` only when
`_is_anthropic_compatible_host()` trusts it, but that check accepted only the
literal `api.anthropic.com` host. Anthropic-compatible gateways that expose the
native Messages protocol under a `/anthropic` path suffix (MiniMax, Zhipu GLM,
LiteLLM-style relays, self-hosted proxies) were rejected, so every auxiliary
call (title generation, memory extraction, vision, reflection) and the
`provider: anthropic` fallback chain discarded the configured base_url and fell
back to `https://api.anthropic.com`. That diverges from the primary path, which
already trusts the `/anthropic` suffix via
`runtime_provider._detect_api_mode_for_url`, and fails outright when the gateway
(not Anthropic) holds the credentials.

Accept `/anthropic` and `/anthropic/v1` suffixed URLs in
`_is_anthropic_compatible_host()`, matching the primary-path convention and
`_wrap_if_needed`. A bare non-Anthropic base_url (e.g. `openrouter.ai/api/v1`
left on `provider: anthropic`) still returns False, preserving the #52608 guard.
2026-08-13 11:41:59 -07:00
Teknium 56f7ccd7a6 test(kanban): opt wake-scope subscriptions into notify+wake delivery mode
The delivery_mode gate on push-adapter wake injection landed on main after
PR #78391 branched; the plain 'notify' default never reaches the wake path
these tests assert. Salvage adaptation for salv-78391.
2026-08-13 11:41:19 -07:00
Nikita Barkov 5d3f75110a fix(kanban): key terminal-event wakes to the creator's workspace scope
Slack session keys include the workspace id since #70190, but the kanban
notifier rebuilds the wake source from a subscription row that has no scope
column, so every terminal-event wake keyed without the workspace.

The legacy-key adoption shipped in the same change (`_legacy_slack_session_key`,
`_recovered_row_matches_source_scope`) resolves that unscoped key onto the same
session_id, so the wake passes the busy guards that are keyed by routing key
(`_active_sessions`, `_running_agents`) and only collides afterwards, on session
id, under the per-session turn lease (#64934) — which serializes it behind the
live turn's flush. On a live Slack gateway that shows up as a duplicate run on
one task plus 400+s of waiting before the woken turn starts.

Same failure mode as #56580 / #72191 (chat_type), one field over, and it needs
no schema change: `_thread_metadata_for_source()` already stamps
`slack_team_id`, the notify subscription persists that dict as
`delivery_metadata`, and the notifier already unpacks it. Rows written by
`kanban_tools._maybe_auto_subscribe` carry no workspace, so fall back to the
adapter's channel → workspace map via `scope_id_for_chat()`, read with getattr
so adapters opt in and unscoped platforms' keys stay byte-identical. Slack
answers it from `_remember_channel_team`, which drops channels claimed by two
workspaces, so an unknown or ambiguous channel degrades to today's behavior
instead of guessing wrong.

Also adds the contributor email mapping the attribution check requires.

Co-authored-by: Junie <junie@jetbrains.com>
2026-08-13 11:41:19 -07:00
brooklyn! 6a198f8a12 fix(install): a failed Node dependency install now fails the install instead of printing success (#85537)
* fix(install): fail when Node dependencies cannot install (#85297)

The POSIX installer converted root and TUI npm failures into warnings, then
printed a dependency-success message and reached the installation-complete
banner with a zero exit status. This left consumers with no usable
node_modules while reporting success.

Treat both required npm installs as fatal: log an error, restore tracked
lockfile churn, return status 1, and propagate the failure from the monolithic
and node-deps stage callers. Successful installs, Termux and missing-Node
skips, missing-manifest skips, and optional Playwright/Browser Use/Computer
Use best-effort behavior remain unchanged. The fix is limited to the POSIX
installer; the PowerShell installer is outside this issue's scope.

Focused and adjacent installer tests passed (32), with bash syntax,
py_compile, and diff checks clean. The broader installer family had 90 passes,
one unrelated pre-existing failure, and two skips; the full suite was
environment-limited by missing dependencies. CodeRabbit, iterative deep
security/compatibility reviews, and final confidence security/compatibility
reviews were clean against the final diff.

Fixes #85297

* fix(install): require npm alongside node in check_node (#77003)

A stray `node` symlink without a sibling `npm` (leftover from a node
version manager) made check_node report "Node.js found"; every later
npm install then failed and the desktop build died with an opaque
"Node.js / npm unavailable". Node now only counts as found when npm
resolves on the same PATH, with an explicit "stray node symlink?" branch
that falls through to the Hermes-managed Node (which bundles npm).

The overlapping success-log honesty half of the original PR is subsumed
by the previous commit, which makes a failed npm install fatal rather
than conditionally-logged; the behavioral tests there cover it, so this
commit keeps only the check_node PATH-gate assertions.

Fixes #77003.

Co-authored-by: criptogus <criptogus@users.noreply.github.com>

---------

Co-authored-by: Eugeniusz Gilewski <egilewski@egilewski.com>
Co-authored-by: CriptoGus <128640021+criptogus@users.noreply.github.com>
Co-authored-by: criptogus <criptogus@users.noreply.github.com>
2026-08-13 18:39:54 +00:00
fangliquanflq d0bb377c96 fix(installer): recover Windows setup when node-deps host exits abruptly (#81390)
* fix(installer): retry abrupt stage host exits

* fix(installer): preserve cancellation across stage retries
2026-08-13 13:38:44 -05:00
brooklyn! d753957e8a fix(install): Windows setup no longer hangs forever on Node.js dependencies (#85529)
* fix(install): time-box the Windows node-deps stage so a stalled npm or Playwright install can't hang setup forever

scripts/install.sh has bounded this same work with run_with_timeout
"$NODE_DEPS_TIMEOUT" (600s default) since #39219, but install.ps1 never got
the guard: Install-NodeDeps ran both `npm install` and `npx playwright
install chromium` unbounded. A stalled registry fetch or a wedged Chromium
archive extraction (#76222, #84614) froze the installer indefinitely -- one
user left it running 12+ hours overnight before asking for help.

Route both invocations through _Invoke-NativeWithTimeout: cmd.exe launches
the native command with its output merged to a log, the parent polls with a
wall-clock deadline and tails new log lines to the console each tick (the
live progress that makes a 3-minute download distinguishable from a hang),
and on timeout taskkill /T /F kills the real process tree and returns 124 --
the same convention as coreutils timeout and bash's run_with_timeout.
Wait-Job was rejected for this: jobs swallow live output and Stop-Job leaves
the npm child running. Windows PowerShell 5.1-safe throughout.

Timeouts surface as a warning with the log path, a note that re-running the
installer resumes (stages are idempotent), and the NODE_DEPS_TIMEOUT env
override for slow links -- mirroring bash.

Fixes #76222.
Closes #84614.
Supersedes #76303.

Co-authored-by: JonthanaHanh <JonthanaHanh@users.noreply.github.com>

* fix(installer): roll stage timers over to hours so an overnight stall doesn't read as "744 hours"

formatElapsed rendered a running stage as m:ss with unbounded minutes: a
node-deps stage left hanging overnight showed "744:38", which the user who
reported the hang understandably read as 744 hours. formatDuration
(completed stages) had the same unbounded-minutes shape.

Move both formatters into src/lib/format.ts (pure, no React) and add the
hour rollover: h:mm:ss live, "Xh Ym" completed. tests-js pins the shapes,
including 744m38s -> 12:24:38.

---------

Co-authored-by: JonthanaHanh <JonthanaHanh@users.noreply.github.com>
2026-08-13 13:38:20 -05:00
Brooklyn Nicholson fa92aa1984 fix(desktop): keep a markdown render failure inside its own message
MessageRenderBoundary re-throws anything that is not the transient
assistant-ui lookup race, by design, so a RangeError raised inside
Streamdown's render unwinds all the way to the workspace boundary and
replaces the entire app with "workspace failed to render". The message
is replayed from the session on every reload, so Retry lands on the same
content and fails the same way — the app is bricked, not glitching.

Wrap the markdown surface itself, so one bad message degrades to the
existing HugeTextFallback (readable, already used for oversized text)
while the rest of the transcript stays alive. The boundary sits on
MarkdownTextSurface rather than any single caller because the crash is a
property of the content, not of which part carries it: the same payload
arrives as an assistant answer, as reasoning, or in tool output, and all
of them render through here.

Tests drive the real component with both known overflow shapes and fail
with the reported RangeError when either half of the fix is reverted.
The depth clamp handles the raw-HTML cause; deeply nested block
structure recurses in mdast-to-hast where no HTML guard can reach it,
which is why the boundary is not redundant.

Co-authored-by: Gille <helix4u@users.noreply.github.com>
2026-08-13 13:36:54 -05:00
Brooklyn Nicholson 1641512c94 fix(desktop): bound raw HTML nesting before it reaches rehype-raw
Streamdown parses assistant markdown with allowDangerousHtml, so every
`<tag>` run in a message goes to parse5 and then through
hast-util-from-parse5, which recurses once per level of unclosed
nesting. Past roughly 1,750 consecutive unclosed tags that overflows the
call stack and throws RangeError out of the middle of a React render.

Nemotron-3-ultra degenerates into exactly that: thousands of `<unk>`
tokens emitted as reasoning, every one of them an element parse5 opens
and never closes. The payload is persisted to the session, so the throw
comes back on every reload.

Clamp the depth of unclosed elements in the prose path and escape the
opening `<` past the cap, leaving the text visible as the literal
`<unk>` it always was. The bound is on depth, not size: 20,000 balanced
`<b>x</b>` pairs and 20,000 void `<br>` tags parse fine because neither
drives the tree deeper, so only unclosed elements are counted and normal
markup is returned by identity.
2026-08-13 13:36:54 -05:00