Commit Graph

20051 Commits

Author SHA1 Message Date
Ben Barclay 46dda84bd7 diag(gateway): temporary relay inbound src-keys logging
Throwaway instrumentation to see whether prospective_thread_id arrives on the
wire from the connector. REVERT after diagnosis.
2026-08-01 19:50:00 -07:00
Ben Barclay af93cb33cb feat(gateway): key Discord auto-thread sessions on prospective_thread_id
Live staging (2026-08-02): only the FIRST auto-thread in a channel got an
auto-title/rename. Root cause is a grouping-model mismatch — the connector
auto-threads per message (each channel message spawns its own thread), but the
gateway keyed sessions per PARENT CHANNEL, so every message after the first
reused the first message's already-titled session; auto-title short-circuited
and the rename lane never fired for later threads.

Intended model: a channel message INITIATES a session, the thread CONTINUES it.
A Discord thread created from a message reuses that message's id as the thread
id, so the connector can tell us the thread id at inbound (before the thread
exists). The paired connector change stamps it as source.prospective_thread_id;
this keys the session on it:

- SessionSource.prospective_thread_id (new field; to_dict/from_dict + the relay
  ws_transport inbound source build read it off the wire).
- build_session_key: effective_thread_id = thread_id or prospective_thread_id.
  The channel-initiating message (no thread_id, carries prospective) and the
  later follow-ups that arrive IN that thread (real thread_id ==
  prospective_thread_id) now produce the SAME key. A real thread_id always
  wins. The chat_type slot is normalized to "thread" when keying on a
  prospective id so the initiating "group"/"channel" event byte-matches the
  follow-up "thread" event. Prospective-thread sessions are shared across
  participants like any thread (not per-user).

Net effect: each distinct channel message is its own session/thread and gets
its own title + rename; follow-ups inside a thread continue that session with
full history. Additive and inert until the connector sends the field, so
non-relay and pre-deploy behaviour is byte-identical.

Tests: initiate-then-continue share one session; distinct channel messages get
distinct sessions; real thread_id wins over prospective; prospective sessions
shared across participants. Session suite 59 passed; relay suite green; ruff +
footguns clean.

Paired: gateway-gateway stamps prospective_thread_id per auto-threading instance.
2026-08-01 18:34:01 -07:00
brooklyn! eca996aa33
Merge pull request #76401 from NousResearch/bb/composer-link-open
Act on composer directive chips from a hover pill
2026-08-01 20:29:14 -05:00
hermes-seaeye[bot] 777512b760
fmt(js): `npm run fix` on merge (#76498)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-02 01:23:45 +00:00
Brooklyn Nicholson 5ba2564ca0 feat(desktop): act on composer directive chips from a hover pill
A directive chip (`@url:`, `@session:`) reads as the thing it points at and is
coloured like one, but a composer is an editor — a click inside the
contenteditable only places the caret, so there was no way to actually act on
the reference.

Hovering a chip whose kind has an action now floats a pill above it that runs
it: `@url:` opens in the browser, `@session:` opens the session as a tab. It's
a small registry (`DIRECTIVE_ACTIONS`), so a new actionable kind is one entry,
not another watcher.

The pill portals to `<body>` and anchors to the chip's rect, so it can't end
up inside the submitted draft, and it re-anchors on scroll and resize rather
than stranding itself over a reference that moved or was deleted. The press is
swallowed before it reaches the editor — mousedown in a contenteditable moves
the caret, and the edit composer reads a blur as "cancel".

Listeners bind to `document`, not the editor: the edit composer's
contenteditable isn't reliably attached when the effect first runs, so an
editor-bound listener never fired there. A document listener that reads the
editor lazily works in both composers, and each instance filters to its own
editor so one chip never shows two pills.
2026-08-01 20:21:28 -05:00
brooklyn! baec57de66
Merge pull request #76429 from NousResearch/bb/composer-placeholder
fix(desktop): the composer hint stops acting like text you typed
2026-08-01 20:14:06 -05:00
brooklyn! cb2311fe2b
Merge pull request #76493 from NousResearch/bb/sqlite-repair-locked
fix(managed_uv): keep project uv config on the candidate locked sync
2026-08-01 20:08:36 -05:00
Brooklyn Nicholson aaa6a97378 fix(managed_uv): keep project uv config on the candidate locked sync
The SQLite runtime repair staged its replacement environment with
`uv sync --extra all --locked --no-config`, and managed_python_env also
exports UV_NO_CONFIG=1. Both drop `[tool.uv]` from pyproject.toml —
including `exclude-newer = "14 days"`, which uv.lock was generated with.

uv 0.12 treats the missing setting as a resolver change, re-resolves, and
then refuses to write under `--locked`:

  Resolving despite existing lockfile due to removal of global exclude newer
  error: The lockfile at `uv.lock` needs to be updated, but `--locked` was provided.

So every repair attempt failed at the dependency-sync gate and reported
"replacement environment did not pass dependency and import smoke tests",
leaving vulnerable-SQLite installs stuck on journal_mode=DELETE with a
guaranteed-failure warning on each `hermes update`.

Drop `--no-config` from the sync argv and pop UV_NO_CONFIG from its env.
Interpreter provisioning keeps both: only the sync has to agree with the
lockfile the project shipped.
2026-08-01 20:02:43 -05:00
Ben Barclay 3f497e2b4f
fix(gateway): relay thread-rename must carry the parent-channel discriminator (#76465)
Live staging (2026-08-01, on a fresh instance where title generation
finally succeeded): the rename lane fired end to end, but the connector
declined the op with "discord egress declined: target not routed to an
onboarded tenant". The trace logs added earlier pinpointed it:

  discord auto-thread rename: thread=... lane=relay new_title='...'
  relay thread_rename declined ...: target not routed to an onboarded tenant
  discord auto-thread rename result: thread=... applied=False

Root cause: the connector's routedEgressGuard resolves the owning tenant
from the outbound metadata's scope_id (guild) or user_id (author). The
adapter builds those via _with_scope(chat_id), reading per-chat caches
keyed by the PARENT channel chat_id learned at inbound. The relay rename
lane called rename_thread WITHOUT parent_chat_id, so chat_id defaulted to
the THREAD id — a key the caches never held — and the op shipped with no
discriminator. resolveTenant returned undefined and egress was declined
before the op ever reached the (now-durable) no-clobber guard.

This was the true terminal blocker: every earlier fix (send-result
feedback, registration/poll ordering, connector-owned guard, durable
Redis store) was correct but sat DOWNSTREAM of this egress-routing
decline, so none of them could take effect.

Fix: the relay lane passes parent_chat_id=source.chat_id (the relay
source's chat_id IS the parent channel; the thread came from send-result
feedback). _with_scope then resolves scope_id/user_id from the
parent-channel caches and the connector routes the op to the tenant.
Scoped to the relay lane only (use_connector_guard); the native lane
renames via the direct Discord API and needs no discriminator.

Tests: adapter-level — a rename passing parent_chat_id carries the cached
scope_id, one keyed on the thread id alone does not (the regression
shape); lane-level — the late-feedback test now asserts parent_chat_id
flows through as the parent channel. Relay suite 150 passed; ruff +
footguns clean.

Connector-compatible with the deployed egress guard; no gateway-gateway
change needed.
2026-08-01 17:08:29 -07:00
Teknium 38c09e5d73 fix(tool-executor): emit tool results on hard interrupt to keep alternation
The sequential executor's KeyboardInterrupt handlers emitted a cancelled
post-tool-call event for the current tool, called agent.interrupt(), then
re-raised — WITHOUT appending a tool result message for the interrupted call
or any remaining calls in the batch. The assistant tool-call turn was left
with no matching tool results, a message-role alternation violation that
malforms the next provider request (relying on downstream repair passes to
patch it, which don't run on every path).

The cooperative-interrupt block (_interrupt_requested) and the concurrent
executor already emit a result for every call_id; this brings the two hard-
interrupt handlers into line via a shared _append_cancelled_tool_results
helper that appends a cancelled result for the current + remaining calls
before re-raising.

Verified live before/after (0 tool results -> 3 for a 3-call batch
interrupted on the first tool) and with a sabotage-checked regression test.
52 interrupt/executor tests pass.
2026-08-01 16:42:57 -07:00
Teknium 8e2997125f chore: map salvaged contributor emails (keepConcentration, JoaoMarcos44) 2026-08-01 16:42:51 -07:00
webtecnica ed1170cd8b fix(config): make get_env_value scope-aware — the last scope-blind credential reader
Salvaged premise from #67065 (@webtecnica, issue #67027), reimplemented:
get_env_value() read os.environ first with no secret-scope check, so a
multiplexed profile turn could serve another profile's credential. Its
siblings get_env_value_prefer_dotenv and gateway.config._getenv were
already scope-aware.

Reimplementation note: the original diff called get_secret() but fell
through to os.environ on a scoped miss — re-opening the exact leak it
targeted (flagged by the sweeper review). This version delegates policy
fully to agent.secret_scope.get_secret (global vars pass through; scope
authoritative under multiplexing; legacy environ behavior when off;
UnscopedSecretError propagates fail-closed), then falls back to .env.

6 regression tests incl. the #67027 repro (envless profile + multiplexed
turn -> None, not the other profile's key); sabotage-verified RED on the
old implementation.
2026-08-01 16:42:51 -07:00
joaomarcos 18e0683bfc fix(auth): route anthropic adapter credential reads through the profile secret scope
Salvaged from #51604 (@JoaoMarcos44, issue #51603): resolve_anthropic_token()
and run_oauth_setup_token() in agent/anthropic_adapter.py read
ANTHROPIC_TOKEN / CLAUDE_CODE_OAUTH_TOKEN / ANTHROPIC_API_KEY via bare
os.getenv(), bypassing agent.secret_scope — a cross-profile over-read in
multiplex mode. Every other provider routes through
runtime_provider._getenv -> get_secret; the adapter now does the same via
a local _getenv wrapper (identical to os.getenv when multiplexing is off,
scope-authoritative + fail-closed when on).

Dropped from the original PR: the cron scheduler hunks (superseded by
fdab380a1a which installs the per-job profile scope) and the unrelated
hermes_logging Windows hunk (scope creep).

Includes the PR's RED->GREEN scope-isolation test file (6 tests).
2026-08-01 16:42:51 -07:00
Teknium fe5a718c4e fix(env): narrow startup env scrub to profile-managed ACP keys
The salvaged cleanup (#75197) scrubbed every known Hermes key absent from
the profile .env — deleting user-shell-exported credentials
(export OPENAI_API_KEY=...) on every hermes invocation, a documented flow
the author's own failing test_dump_flags_shell_only_key_not_in_dotenv
confirmed. A child process cannot distinguish shell exports from
parent-process leakage, so the scrub now covers ONLY
_PROFILE_MANAGED_ENV_KEYS (ACP routing keys: HERMES_ACP_*,
HERMES_COPILOT_ACP_*, COPILOT_CLI_PATH, COPILOT_ACP_BASE_URL) —
the vector from #75141. Cross-profile credential isolation is owned at
read time by agent.secret_scope.get_secret.

Adds shell-export survival regression + a scope-invariant test that fails
if the scrub set is ever widened toward credential-shaped keys.
2026-08-01 16:42:51 -07:00
keepConcentration 61b2fa7937 fix(env): strip export prefix in dotenv key scan for cleanup (review fix) 2026-08-01 16:42:51 -07:00
keepConcentration 968b66338c fix(env): clear inherited Hermes keys missing from profile .env (ACP leak)
Align load_hermes_dotenv() with reload_env() so known Hermes env vars
absent from the active profile .env are removed from os.environ instead
of leaking from a parent process / other profile.

Register ACP-related keys (HERMES_ACP_AUTH_METHOD, HERMES_COPILOT_ACP_*,
COPILOT_CLI_PATH, COPILOT_ACP_BASE_URL) in _EXTRA_ENV_KEYS so they
participate in known-key cleanup.

This is the same isolation gap class as #68367 / #66930, but:
- Not Desktop-only spawn scrub — CLI/gateway restart inheritance
- Not Matrix/messaging auto-enable only — copilot-acp provider/ACP config
- Startup dotenv clear so *any* inheritance path is covered

Example: HERMES_ACP_AUTH_METHOD=cursor_login leaking into a Claude Code
ACP profile caused authenticate -> Internal error -> Discord
'model provider failed after retries'.
2026-08-01 16:42:51 -07:00
Teknium 6b519255ea fix(update): provision a managed Node runtime when system npm fails engines.npm
The npm 12 requirement (f88ed6c717) strands every system-Node install:
no shipping Node bundles npm >=12, engine-strict makes EBADENGINE fatal,
and the recovery in npm_engine.py refuses to touch a system npm — so
'hermes update' leaves the install in a mixed state (updated code, stale
Node deps, no TUI/web/desktop rebuild) with only a manual-fix hint.

Instead of modifying the user's toolchain (still never done), the
EBADENGINE recovery now provisions Hermes' own managed Node tree under
$HERMES_HOME/node — the same pinned-nodejs.org path install.sh and
install.ps1 use — upgrades THAT npm into the required range, and hands
the caller the managed npm for its single retry.

- hermes_constants.bootstrap_hermes_managed_node(): cross-platform
  provisioning (POSIX via node-bootstrap.sh _nb_install_bundled_node,
  Windows via the existing portable-zip download); reuses a healthy tree.
- node-bootstrap.sh: HERMES_NODE_SKIP_LINKS=1 skips the ~/.local/bin
  node/npm/npx symlinks so the private tree never shadows the user's
  own toolchain on PATH.
- maybe_repair_npm_engine() now returns the npm path to retry with
  (managed-in-place upgrade or freshly provisioned runtime); both call
  sites retry with the returned path and put the managed tree first on
  PATH so npm lifecycle scripts resolve the managed node.
- Node-only mismatches on a foreign npm are now also recoverable (the
  managed tree ships a supported Node); on a managed npm they still
  correctly decline.

E2E (real download, temp HERMES_HOME): provisioned node v22.23.2,
upgraded bundled npm 10.9.4 -> 12.0.2, system npm byte-identical after,
no ~/.local/bin links re-pointed, healthy-tree reuse in 0.05s.
2026-08-01 16:40:50 -07:00
Teknium 7f4d155159 fix(tools): validate timeout, reject whitespace old_string, narrow /private/var block
Three lower-severity core-tool robustness fixes from a targeted audit, each
reproduced live:

1. terminal_tool did not validate non-positive timeouts. 'timeout or default'
   silently coerced 0 to the config default (0 can't mean 'no timeout'), and a
   negative value is truthy so it flowed into 'deadline = now + timeout' and
   fired an immediate '-Ns' timeout. Reject timeout <= 0 with a clear message.

2. fuzzy_find_and_replace accepted a whitespace-only old_string, which matches
   trivially (blank line / run of spaces) and mass-replaces under replace_all
   or raises an opaque ambiguity error. Reject it alongside the empty check.

3. The '/private/var/' sensitive-path prefix over-blocked ALL macOS temp-file
   writes: , /tmp, and /var/folders realpath into /private/var/folders
   on macOS (and paths are resolved through symlinks), and /private/var/tmp is
   a normal temp dir. Narrowed to the genuinely-sensitive subtrees
   (/private/var/db, /private/var/root); /etc and /private/etc stay blocked.

All verified with sabotage-checked regression tests. 85 terminal/fuzzy/file
tests pass; normal timeouts, legit replacements, and /var + /boot + /etc
blocking are unaffected.
2026-08-01 15:41:21 -07:00
Teknium 62f00319db fix(patch-parser): tolerate CRLF patch bodies and Move-then-Update
Two V4A parse/validate bugs found in a core-tools audit, reproduced live:

1. CRLF patch body injected stray carriage returns. parse_v4a_patch split
   on '\n' only, so a CRLF-encoded patch kept '\r' inside every HunkLine
   and wrote mixed line endings into an LF file; the anchored Begin/End
   markers could also fail to match because of the trailing '\r'. Strip a
   trailing '\r' from each line at split time.

2. Move-then-Update of the same file was rejected. _validate_operations read
   the UPDATE target from disk before the MOVE ran, so 'Move a->b' + 'Update
   b' failed validation with 'b: file not found'. Added a small pending-move
   overlay so UPDATE/DELETE/MOVE reads during validation see prior ops'
   effects (moved-in destinations resolve, moved-away sources read as gone),
   while a genuine 'destination already exists' conflict is still caught.

Both verified with sabotage-checked regression tests. 113 patch/fuzzy/file
tests pass.
2026-08-01 15:40:39 -07:00
Teknium c0b0c88626 fix(fuzzy-match): stop context_aware from silently replacing wrong content
Strategy 9 (context_aware, the last-resort fuzzy strategy used by
patch_replace, V4A UPDATE hunks, and skill_manage) had two serious flaws,
both reproduced live against current main:

1. CORRECTNESS: it accepted a block when >=50% of its lines were >=0.80
   similar. A 2-line pattern with one real line and one garbage line matched,
   silently deleting the non-matching line and persisting a wrong edit as
   success. Now requires the first AND last lines to anchor-match and EVERY
   non-blank pattern line to be >=0.80 similar — one garbage line disqualifies
   the block.

2. PERFORMANCE: it scored every content window with per-line SequenceMatcher,
   so every failed match paid O(file_lines x pattern_lines) — measured ~5.5s
   for a single 40-line no-match on a 10k-line file, per hunk. The first/last
   line anchor pre-filter skips non-candidate windows: same case now ~160ms
   (34x faster).

Also gate replace_all: a similarity-based strategy (block_anchor,
context_aware) with multiple matches under replace_all would overwrite every
approximate block, not just exact ones. Now refused with a clear error
directing the caller to precise text.

All verified with sabotage-checked regression tests (fail against the old
50% logic). 158 file/patch/fuzzy tests pass; legit fuzzy edits (indent drift,
unique near-match) unaffected.
2026-08-01 15:40:13 -07:00
Teknium 021a076880 fix(file-ops): prevent non-UTF-8 corruption and symlink data-loss
Two DATA-LOSS bugs in ShellFileOperations found in a core-tools audit,
each reproduced live against current main:

1. Non-UTF-8 file content silently corrupted on read->write. The terminal
   env decodes stdout with errors='replace', so a latin-1/8859 file's bytes
   arrive as U+FFFD before _is_likely_binary inspects them. U+FFFD is
   'printable', so the >30%-non-printable check never flagged it, and the
   agent would read the mojibake and write it back, permanently replacing the
   original bytes. Fix: treat a sample containing U+FFFD as binary (read-only).

2. Writing through a symlink destroyed the link and orphaned the target. The
   atomic temp-file + 'mv -f' swap replaced the symlink itself with a plain
   file; the real target was never updated. Fix: resolve the link with
   readlink -f/realpath first and recompute the temp dir from the resolved
   target so the mv stays same-filesystem atomic. Broken links fall back to
   the original path (no regression).

Both verified with sabotage-checked regression tests (fail without the fix).
Proper UTF-8 text (incl. non-ASCII) and plain-file writes are unaffected.
2026-08-01 15:39:33 -07:00
Teknium 9d08c95464 fix(tools): dedup eviction task_id + workdir cwd leak
Two independent HIGH-severity correctness bugs found in a core-tools audit,
each reproduced live against current main:

1. Read-dedup was never evicted after a write on non-default tasks.
   _invalidate_dedup_for_path looked up the read-tracker under the correct
   task_id but resolved the path with _resolve_path(filepath) — which
   DEFAULTS task_id='default'. The dedup cache is keyed by the task-resolved
   absolute path, so for any task whose workspace cwd differs from the process
   cwd (every -w worktree / Desktop / ACP session using relative paths) the
   computed key never matched and the stale entry was never removed. A
   read_file after a write_file/patch could then return the OLD content stub
   when mtime coincided. Fix: pass task_id through.

2. A per-command workdir override permanently hijacked the session cwd.
   The post-command dual-write unconditionally recorded env.cwd (stamped to
   the transient workdir) into the durable session-cwd store, so every later
   command that omitted workdir inherited the one-off directory — contradicting
   the documented 'Working directory for this command' contract. Fix: skip the
   session-cwd record when workdir was explicitly supplied.

Both verified with sabotage-checked regression tests (fail without the fix).
2026-08-01 15:38:57 -07:00
Teknium 9467e99ac5 docs(site): redirect /quickstart and /installation short paths
Users following abbreviated links guess /docs/quickstart and
/docs/installation and hit raw GitHub-Pages 404s — the real pages live
under /docs/getting-started/. Add client redirects for both.

Consumer-onboarding audit finding #1, Aug 2026.
2026-08-01 15:34:27 -07:00
Teknium 38453baeee fix(setup): warn loudly when the wizard finishes without a working provider
Cancelling the API-key prompt mid-wizard (Enter → 'Cancelled.') let the
wizard continue through Terminal/Gateway/Tools and finish 'successfully'
with no model configured — the user exits believing they're set up, then
hits a broken chat.

_print_setup_summary() (called by every setup path: full, quick,
blank-slate, portal) now probes resolve_provider() and, when nothing is
configured, prints an unmissable warning with the two one-line fixes
(hermes model / hermes setup --portal).

Consumer-onboarding audit finding #7 (sev 4), Aug 2026.
2026-08-01 15:34:23 -07:00
Jaret Bottoms eec6d3efde fix(teams): suppress SDK import-time dotenv instead of clearing environ
Teknium review on #62947: os.environ.clear()/update around deferred
loaders is unsafe under concurrency and misses teams_pipeline's direct
adapter import.

Defer microsoft_teams binding in the Teams adapter, no-op
dotenv.load_dotenv while the SDK imports, keep api_server explicit
disable, and add SDK-import + load_gateway_config canaries.

Fixes #62935
2026-08-01 15:34:19 -07:00
KaliShodan a98c8eeed1 fix(gateway): isolate deferred platform imports from os.environ leaks
microsoft-teams-apps calls load_dotenv(find_dotenv(usecwd=True)) at import time, which can pull a root-profile .env into every gateway process during plugin_entries() discovery and break profile secret isolation.

Snapshot/restore os.environ around deferred loaders, and honor explicit api_server enabled:false the same way _enable_from_env does for other platforms.

Fixes #62935
2026-08-01 15:34:19 -07:00
Teknium d7522118ef fix(cli): route keyless first run into provider onboarding instead of a broken chat
A completely unconfigured install previously booted into a working-looking
chat (banner showed model 'unknown'), accepted a message, spun ~30s, then
failed with 'Set OPENROUTER_API_KEY' — a provider the user never chose —
and never offered setup.

- HermesCLI.run() now probes provider readiness at startup (TTY only) and
  offers the shared provider picker (hermes model flow, which fronts Quick
  Setup / Nous Portal OAuth) when nothing is configured. Decline is
  respected; picker state re-syncs into the live CLI so the next turn works
  without a restart.
- New silent probe _runtime_credentials_ready(): no printing, no state
  mutation; handles keyless local endpoints and callable bearer providers.
- The empty-api-key error is provider-aware: names the actual resolved
  provider and points at 'hermes model' / 'hermes setup' instead of
  hardcoding OPENROUTER_API_KEY.
- Banner: unconfigured installs render 'no model configured — run /model'
  in red instead of the silent 'unknown' model slug.

Consumer-onboarding audit finding #2 (sev 5), Aug 2026.
2026-08-01 15:34:16 -07:00
Teknium cc0af6b9e8 ci: skip Desktop E2E + Docker build on tests-only PRs (python_prod lane)
After the test-suite prune, the Python slices (~2.3m each) are no longer
CI's critical path — Desktop E2E (5.2m, the longest job) and the Docker
build are, and both run on every python-lane PR even when the diff never
leaves tests/. Neither consumes the test suite: Playwright drives the
built app + hermes serve backend, and the image copies installed code.

New python_prod lane = python minus tests-only diffs. e2e-desktop and
docker gate on it; every pytest/lint lane keeps gating on python.
Fail-open contract preserved: .github/ changes and empty diffs set
python_prod=true, and runner infrastructure (scripts/run_tests.sh,
run_tests_parallel.py) is deliberately NOT tests-only since a bad
runner edit can mask real failures.

Replay over the last 231 main commits: 39 (17%) would skip both jobs,
cutting their critical path from ~8m to ~3m. E2E-verified through the
real script entrypoint (tests-only/prod/mixed/fail-open) + 83 tests/ci
green.
2026-08-01 14:59:14 -07:00
Brooklyn Nicholson 93ec02bf79 fix(desktop): keep the hint off the text during IME composition
Input events are deliberately skipped for the duration of an IME composition
(they carry uncommitted preedit text), so nothing clears the empty marker
until compositionend — the hint kept painting behind the hiragana the user was
composing. Drop the marker as composition starts; the normalizer restores it
if composition ends with nothing committed.

Taking the hint out of the text flow fixed the displacement half of #75960 on
its own — preedit now starts at the field's left edge either way — but the
overlap needed this too.

Co-authored-by: Ryuichi Natori <to-na@users.noreply.github.com>
2026-08-01 16:56:48 -05:00
brooklyn! 97971643ab
Merge pull request #76417 from NousResearch/bb/kanban-model-picker
Pick a kanban task's model and thinking depth from the board
2026-08-01 16:55:01 -05:00
dsad a5f94e93ad fix(acp): bind session_id in session context for subprocess isolation
The ACP prompt path called set_session_vars(session_key=session_id, ...)
without passing session_id, so the HERMES_SESSION_ID ContextVar was bound
to its explicit "" default. Once the session-context machinery is engaged,
_inject_session_context_env treats an explicitly-bound "" as authoritative
and writes it to the child env — so subprocesses spawned during an ACP turn
got an empty HERMES_SESSION_ID instead of the session's own id.

Pass session_id through so child subprocesses carry the correct id.

Salvage of the ACP half of #53454 by @necoweb3 (the V4A-path half is
salvaged separately in the file-tools PR).

Co-authored-by: necoweb3 <sswdarius@gmail.com>
2026-08-01 14:53:34 -07:00
Teknium ff6ed7c491 fix(update): detect EOL-only churn via numstat, not name-only
_normalize_managed_eol isolated line-ending churn from real edits by
diffing twice: all dirty files minus files still dirty under
--ignore-cr-at-eol. But 'git diff --name-only --ignore-cr-at-eol'
computes its file list from blob/stat differences BEFORE the CR filter
is applied, so it still lists CR-only files. On git 2.48.1 the two
name-only sets are therefore identical, _eol_only() is always empty, and
a managed Windows checkout gets pinned to core.autocrlf=false with the
whole CRLF tree left dirty — breaking the next 'git checkout' on update
(the exact failure this function exists to prevent).

Compute the real-edit set with 'git diff --numstat --ignore-cr-at-eol'
instead: numstat honors the CR filter (a CR-only file produces no
record), so eol-only files are correctly identified and cleared while
genuine edits are preserved. Pin core.quotepath=false so non-ASCII paths
parse. Verified at 1200 files: 1199 eol-only normalized, one real edit
preserved, autocrlf pinned only after the tree reads clean.

This was a pre-existing failure on main (test_update_eol_churn's
test_churn_across_more_files_than_fit_in_one_argv failed deterministically
on git 2.48.1), surfaced while landing unrelated file-tools PRs.
2026-08-01 14:52:45 -07:00
Teknium 9772e3b189 perf(model-picker): serve stale model caches instantly, refresh in background
The remaining /model picker stall after the Copilot backoff fix: whenever
the 1h provider-models disk cache TTL (or the remote model-catalog manifest
TTL) lapsed mid-session, the next picker open blocked on 8-9 serial
/v1/models round-trips (~2-3s measured) plus the catalog manifest fetch
before rendering anything.

Model catalogs change on release timescales, not hourly — so both caches
now use stale-while-revalidate:

- cached_provider_model_ids(): an expired entry whose credential
  fingerprint still matches is served immediately; a deduped daemon thread
  re-fetches the live catalog and rewrites the disk cache for the next
  open. Entries older than 7 days still block on a live fetch, credential
  rotation still busts the entry, and force_refresh still bypasses SWR.
- model_catalog.get_catalog(): an expired disk manifest is served
  immediately with an off-thread refresh; only a truly cold cache (no disk
  copy) blocks on the network.

Measured picker payload build with deliberately-expired caches:
2.9s -> 0.93s (first open in process) / 0.06s (subsequent opens).
Combined with the Copilot fix (#76386): 7.3s -> ~0.06s for the common case.
2026-08-01 14:52:18 -07:00
Brooklyn Nicholson 414e5af114 fix(desktop): the composer hint stops acting like text you typed
The empty-composer prompt was painted with an inline `::before`, which puts a
real box in the contenteditable's text flow. Click an empty composer and the
caret lands past the hint instead of at the field's left edge, and a hint that
wraps — a narrow composer, a long locale string — makes the empty composer two
lines tall. It is a hint, so it now sits out of flow: absolutely positioned,
clipped to one line, unselectable and untouchable by the pointer. Measured in
Chromium against the built stylesheet, the caret lands at the left edge for a
wide composer, a narrow one, and a Japanese hint alike.

Backspace on a fresh `@folder:` chip had a second, related problem. Committing
a completion empties the typed token's text node rather than removing it, and
`Range.insertNode` splits the line around the caret, so the chip ends up
between zero-length text nodes. Those read as content: the atomic chip-delete
declined, Chromium's own backspace bounced between the leftovers, and the chip
took extra presses to remove — leaving a `"\n"` draft with the hint still
hidden behind it. Emptiness, the chip-delete, and the DOM normalizer now all
step over that litter.

The stylesheet owns the rule now that it needs `position: relative` on the
editor, so the utility-class constant both composers imported is gone.
2026-08-01 16:34:46 -05:00
dsad fcd5e2cc61 fix(file-tools): resolve local V4A patch paths before apply
patch_tool resolved V4A header paths against the task workspace for
locking, staleness, and reporting, but handed the original (often
relative) patch text to file_ops.patch_v4a — which re-resolved headers
against the backend env's own cwd. When the two diverge (the git-worktree
cwd bug), a relative header landed in a different directory than
everything the tool locked and reported: a silent wrong-file write.

Rewrite Update/Add/Delete/Move File headers to the resolved absolute
paths before apply, only for host-filesystem backends (container/remote
namespaces keep their own paths). Header patterns mirror patch_parser
(no-space ***Update File: form) and cover Move File: src -> dst.

Salvage of #53176 by @necoweb3, reimplemented onto current main (the
original branch predates the sensitive-path/Move-header extraction and
per-path locking now in patch_tool).

Co-authored-by: necoweb3 <sswdarius@gmail.com>
2026-08-01 14:31:51 -07:00
spfcraze 8c172726c8 fix(patch): anchor V4A Begin/End Patch markers to full lines
The boundary scan in parse_v4a_patch used substring matching, so a
content line mentioning "*** End Patch" (docs about the patch format,
nested patch text) truncated the patch, and "*** Begin Patch" in
content reset the start boundary — silently dropping already-parsed
operations while reporting success. Match only whole-line markers at
column 0, preserving the no-space "***Begin Patch" tolerance.
2026-08-01 14:31:48 -07:00
Teknium 8b8ebc26bb chore: map contributors arcabotai, Sora-bluesky 2026-08-01 14:30:16 -07:00
Cad from Arca f3cb7c0e01 style(desktop): satisfy mapped-profile test lint 2026-08-01 14:30:16 -07:00
Cad from Arca 5cb19d7af5 fix(desktop): keep Windows SSH runtime at machine root 2026-08-01 14:30:16 -07:00
Cad from Arca 6d3cb23d24 fix(desktop): reject reserved remote profile names 2026-08-01 14:30:16 -07:00
Cad from Arca e2c6f2ebc4 test(desktop): cover mapped non-default SSH profile 2026-08-01 14:30:16 -07:00
Sora-bluesky 621975de85 fix(dashboard): anchor the SSH token dir to $HOME/.hermes, not the active profile
The Desktop client writes the SSH session token under $HOME/.hermes/desktop-ssh
(a literal ~/.hermes/desktop-ssh in apps/desktop/electron/remote-lifecycle.ts,
expanded against the account's $HOME), independent of HERMES_HOME and the active
profile. But _read_ssh_session_token_file validated it against
get_hermes_home()/desktop-ssh, which a non-default sticky profile re-homes to
<root>/profiles/<name>/desktop-ssh (and any custom HERMES_HOME points elsewhere).
relative_to() then rejects every token as "not under the desktop-ssh directory",
so SSH remote mode is broken under any non-default profile.

Anchor to Path.home()/.hermes/desktop-ssh so the validator matches the exact
directory the client writes to, across default, profile, and Docker layouts.
Adds profile / custom-root acceptance tests and a profile-local rejection test.

Fixes #69551.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-01 14:30:16 -07:00
Cad from Arca d6be88fbc8 fix(desktop): map SSH profiles to remote profiles 2026-08-01 14:30:16 -07:00
Flownium 4ebdbadef8 fix(desktop): close wake indicator with main window 2026-08-01 14:30:11 -07:00
Flownium 53b8f44e75 feat(desktop): add notch wake indicator 2026-08-01 14:30:11 -07:00
Teknium ff8df5822b fix(desktop): derive font-setting save base from the config cache, not a mirrored ref
The desktop lint rule added after the original PR bans mirroring reactive
values into refs via useEffect. Rework the setting to seed from draft-null
state, guard profile switches by stale-config identity, and derive the
save base + rollback value from the shared config record instead of
latestConfigRef/lastSavedRef/seededRef.
2026-08-01 14:30:08 -07:00
Zeraphim 261c67b2f8 fix(desktop): cover pending agent terminal font cleanup 2026-08-01 14:30:08 -07:00
Zeraphim 0399711bec feat(desktop): add terminal font picker 2026-08-01 14:30:08 -07:00
baoyu0 131aee9260 feat(desktop): add configurable terminal font family (terminal.font_family in config.yaml)
Adds a new config option terminal.font_family that lets users customize the
CSS font-family for the desktop app's embedded xterm.js terminal.

Previously the font was hardcoded in use-terminal-session.ts:
  'JetBrains Mono', 'Cascadia Code', 'SF Mono', Menlo, Consolas, monospace

Now the value from config.yaml (terminal.font_family) is threaded through:
  useHermesConfig → PersistentTerminal → TerminalTab → useTerminalSession

When font_family is empty or unset (default), the built-in fallback is used,
preserving backward compatibility. Users with Nerd Fonts installed (e.g.
CaskaydiaCoveNerdFont) can now set:

  terminal:
    font_family: 'CaskaydiaCoveNerdFont', 'JetBrains Mono', monospace

Closes: #terminal-font-config
2026-08-01 14:30:08 -07:00
Teknium afdf8f9cc5 fix(model-picker): stop Copilot token-exchange retry backoff from stalling /model open
The no-args /model picker calls list_authenticated_providers(), which walks
every provider through load_pool(). For copilot, _seed_from_singletons()
re-runs the raw-token -> API-token exchange on every pass. When the exchange
is rejected (HTTP 403: token not Copilot-entitled, revoked, org-blocked),
the transient-network retry loop slept ~4.5s (1.5s + 3.0s backoff) before
degrading to the raw token — and nothing cached the failure, so EVERY picker
open, provider discovery pass, delegation spawn, and dashboard credential
listing paid the full 4.5s again.

Measured on a machine with a 403-rejected gh token: /model picker payload
build went from 7.3s to 1.0s cold and 0.06s warm.

Fixes:
- Permanent HTTP rejections (401/403/404) skip the retry backoff entirely —
  the loop exists for startup network races, not auth rejections.
- Negative cache keyed on token fingerprint: failed exchanges are not
  re-attempted for 30min (auth rejection) / 60s (transient network error).
- Success and evict_cached_exchanged_token() both clear the negative-cache
  entry, so the runtime stale-credential recovery path still forces a fresh
  exchange.
2026-08-01 14:29:04 -07:00