Commit graph

17878 commits

Author SHA1 Message Date
hermes-seaeye[bot]
6c9718151c
fmt(js): npm run fix on merge (#71706)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-26 02:46:01 +00:00
brooklyn!
46cae787ab
Merge pull request #71697 from NousResearch/bb/tui-slash-trigger
fix(tui): make skills referenceable anywhere in the composer
2026-07-25 21:38:12 -05:00
Teknium
243a01d5d7 fix(curator): make the autonomous write policy consistent (#67140)
The background write guard decided ownership from `isinstance(usage_rec, dict)`,
so a local skill with NO usage record passed. That successful write called
bump_patch(), which created a `created_by: null` record — and the identical
write was refused from then on. "Allowed exactly once, then never" is a race
with our own bookkeeping, not a policy. Reproduced on main: patch #1 succeeds,
patch #2 with the same arguments is refused.

Option B from the issue. Option A (split `session_review` from
`scheduled_curator` and let the session fork patch user-owned skills it
consulted) would widen autonomous write permission onto skills the user owns
with no user present to consent — wrong direction for a no-user-present actor.

- skill_manager_tool: missing and explicit-null records now resolve
  IDENTICALLY, both fail closed. The refusal names the reason and points at
  `hermes curator adopt <name>`.
- background_review: both review prompts told the reviewer to patch any skill
  consulted in the session and claimed pinned skills could be improved, while
  enforcement refused both. Prompts now list pinned, external, and user-owned
  skills as protected, and tell the reviewer to RECOMMEND adoption instead of
  attempting a write that will be refused.
- skill_usage: document that `created_by` is a curator-management policy flag,
  not a provenance claim, and add `is_curator_managed()` so call sites read as
  the question they ask. Field name retained — it is on disk in every
  `.usage.json` and renaming would strand those records.
- curator CLI: `hermes curator list-unmanaged` itemizes unmanaged skills with
  the reason each is unmanaged (completes the #67139 spec).

Foreground writes are untouched: a user-directed edit to a user-owned skill
still works, including on pinned skills.

Sibling tests: 9 failures in test_skill_manager_tool.py were fixtures that
created record-less skills to exercise OTHER guards (consolidation-delete,
read-before-write) and relied on ownership falling through. Fixed at the
fixture, since the real curator only ever operates on managed sediment. One
test asserted the old "manually authored" wording; rewritten to assert the
behavior contract instead of the string.

Validation: 274 targeted tests + all 7 background-review files (60 tests) pass.
E2E on a temp HERMES_HOME (30 checks) covers the flip, foreground writes,
adoption unblocking, pin semantics, prompt/enforcement parity, and the new verb.
Each new test sabotage-verified: revert the fix, confirm it goes red.

Fixes #67140
2026-07-25 19:27:17 -07:00
brooklyn!
6ab5d2df2a
Merge pull request #71664 from NousResearch/bb/composer-slash-trigger
fix(desktop): make skills referenceable anywhere in the composer
2026-07-25 21:26:14 -05:00
teknium1
cfd2a38223 fix(conversation): anchor the cwd staleness read to the host-info block
The whole-prompt scan for "Current working directory:" matched USER project
content, not just Hermes' own host-info line. The prompt embeds AGENTS.md /
CLAUDE.md / .cursorrules in the context tier, which sits AFTER the host-info
block, and line_value() takes the LAST match — so any project file containing
a line starting with that label won the scan.

The comparison then ran runtime state against project prose, a mismatch that
never clears: the stored prompt was rejected on EVERY turn, rebuilding the
system prompt each message and destroying the prefix cache for the whole
session. Strictly worse than the staleness the check exists to catch, and
invisible to CI because no test encoded the invariant.

"last match wins" was only ever safe because Model/Provider/Platform live in
the volatile tier at the very END of the prompt. The cwd line is the one field
read from the STABLE tier near the start, so it needs an anchored read:
locate Hermes' own "User home directory:" line and take the working-directory
line that follows it.

Verified against the real builder on a real AIAgent, not a stub:
  - stable cwd, clean project           -> prompt REUSED (0 rebuilds)
  - stable cwd, AGENTS.md naming the
    field                               -> prompt REUSED (was: rebuild/turn)
  - drifted cwd                         -> rebuilt
  - drifted cwd, AGENTS.md naming the
    NEW cwd                             -> rebuilt (prose can't mask drift)

Also re-verified no spurious rebuild across all five cwd-resolution paths
(pinned contextvar, TERMINAL_CWD, launch dir, symlinked workspace,
trailing-slash cwd).

The contributor's fixtures omitted the host-info anchor, so they silently
stopped exercising the cwd path once the read was anchored; reshaped them to
match real prompt structure via a _host_block() helper.
2026-07-25 19:25:34 -07:00
MorAlekss
e741dc7d91 test(conversation): add TERMINAL_CWD gateway cwd tests for resolve_agent_cwd staleness check 2026-07-25 19:25:34 -07:00
MorAlekss
7a61b66dac fix(conversation): use resolve_agent_cwd() and Platform line for stored-prompt staleness check 2026-07-25 19:25:34 -07:00
MorAlekss
30cd6e989d test(conversation): add runtime surface drift test for stored-prompt staleness check 2026-07-25 19:25:34 -07:00
MorAlekss
a2b0d6d8e4 fix(conversation): prevent stale prefix cache on cwd or runtime surface change 2026-07-25 19:25:34 -07:00
brooklyn!
3a06908433
Merge pull request #71688 from NousResearch/bb/profile-gateway-inherit-label
fix(desktop): clarify inherited profile gateway
2026-07-25 21:22:41 -05:00
brooklyn!
a135b278dd
Merge pull request #71601 from helix4u/fix/desktop-model-picker-authority
fix(desktop): keep model picker aligned with session state
2026-07-25 21:20:54 -05:00
Brooklyn Nicholson
ec6fa9bdb2 test(tui): cover inline skill references end to end
inlineSlashTrigger and completionRequestForInput are driven through the
mid-message shape, the position-0 shape, and the path cases that must not
claim a slash (/usr/local/bin, src/foo/bar, and/or, 3 /4).

splitSlashSkillRefs asserts the round-trip invariant — the segments always
rejoin to the exact input — rather than freezing a segment list.

The applyCompletion cases reproduce the reported bug rather than restating
the implementation: reverting the fix fails with
`expected 'please run //clean' to be 'please run /clean'`.
2026-07-25 21:19:23 -05:00
Brooklyn Nicholson
07244c5ead fix(tui): keep a referenced skill styled in the sent message
The composer offers a mid-prose /skill as a completion, but the transcript
knew nothing about slash references and rendered the whole user message as
one flat run of text, so the skill lost its accent the moment it was sent.

User messages are now split into plain and reference runs, and the
reference keeps the accent it wore in the composer. The text is unchanged —
concatenating the segments reproduces the input exactly — so this is
presentation only, and the backend still receives the literal /clean.

The scan has to reject a token that continues into a path, since it runs
over finished text rather than anchoring at the caret: /usr/local/bin would
otherwise style as /usr.
2026-07-25 21:19:23 -05:00
Brooklyn Nicholson
530e7c0d4e fix(tui): offer skill completions for a slash typed mid-message
A slash only opened completions at position 0, so `please run /cle`
suggested nothing. That is correct for execution — commands only run from
the start of a message — but it also removed the ability to reference a
skill inside prose.

Position 0 and mid-message are now detected separately. A leading slash
stays a command invocation with arg completion and the full command set; a
slash after whitespace is an inline skill reference. Only skills are
offered there, since a built-in like /model or /new acts on the app and
reads as nothing useful inside a sentence.

The gateway tags each completion with its kind, derived from the same
skill-command and skill-bundle providers the completer already consumes,
so the TUI filters on data rather than sniffing the display meta glyphs.

applyCompletion keyed its leading-slash check off the start of the input,
which is only the replace point for a position-0 command. It now keys off
the character before compReplace, so an inline pick lands as
`please run /clean` instead of `please run //clean`. The Tab handler
carried its own divergent copy of that logic and now calls the shared
helper.
2026-07-25 21:19:23 -05:00
Brooklyn Nicholson
28c12c8c94 fix(desktop): localize the inherited-gateway copy for Arabic
The ar locale landed after the inheritance-label fix was written, so it
still told users to set a named profile to Local to inherit the default
and fell back to English for the renamed card.
2026-07-25 21:10:04 -05:00
KaliShodan
4656a1a009 fix(desktop): clarify inherited profile gateway 2026-07-25 21:06:40 -05:00
brooklyn!
a606d24cf2
Merge pull request #71679 from NousResearch/bb/default-effort
fix(desktop): honor the configured reasoning effort instead of assuming medium
2026-07-25 20:58:00 -05:00
Brooklyn Nicholson
43d1088ba2 refactor(desktop): give reasoning effort one owner
The seven effort levels were enumerated four times in four shapes: a
labels map, a radio-option list, a settings value array, and an enum
list for the config field. Each carried its own hardcoded medium
fallback, which is how the picker drifted from the configured default in
the first place.

Collapse them into lib/reasoning-effort, which owns the scale, the
labels, and the resolve/fallback rules. Net -80/+26 in source. Dropping
the effortLabelKey cast also means the i18n keys are now type-checked
against the canonical list instead of asserted into place.
2026-07-25 20:51:18 -05:00
brooklyn!
10160a1808
Merge pull request #71672 from NousResearch/bb/tab-session-titles
fix(desktop): name a Cmd+T session from its first message
2026-07-25 20:51:01 -05:00
Brooklyn Nicholson
412a535433 fix(desktop): scope mid-message references to skills and keep them as chips
Two corrections to the inline slash reference.

Only skills are offered mid-message now. A built-in like `/model` or `/new`
acts on the app, so it reads as nothing useful in the middle of a sentence —
whereas a skill is exactly the thing you want to point at while describing
work. A leading `/` is unchanged and still offers the full command set.

A picked skill also stays a pill in the sent message. The composer already
inserts one, but the message renderer knew nothing about slash references and
flattened it back to raw text on send. It now parses a mid-prose `/skill` into
a chip segment and renders it with the same styling the composer uses. The
submitted text is untouched — the chip still round-trips to the literal
`/clean` the backend expects — so this is presentation only. Path-like tokens
(`/usr/local/bin`) are excluded: unlike the caret-anchored composer trigger,
this scans finished text, so it also has to reject a token that runs on into
a path.
2026-07-25 20:45:11 -05:00
Brooklyn Nicholson
fab0e60570 fix(desktop): name a Cmd+T session from its first message
A Cmd+T tab's session is created unlisted (openNewSessionTile passes
`listed: false`), so it has no $sessions row until its first turn
persists and a refresh surfaces it. For that whole first exchange the
tab strip and the sidebar read "New session".

Cmd+N has no such gap: its session is created per-send, and
createBackendSessionForSend seeds the optimistic row with the user's
text as the preview. A tile never reaches that path — its runtime id is
already bound, so its createBackendSessionForSend seam is a no-op that
returns the existing id.

Seed the row the same way on a tile's first send. The tab strip already
re-syncs on $sessions (watchSessionTiles lists it in `also`), so the tab
and the sidebar both name the session within the first message; the
server's auto-title supersedes the preview when the turn completes.

Guarded so it only ever fires once per session: no-op on empty text and
on an already-listed row, matching across compression by lineage root so
a re-send can't clobber a real title with a raw message preview.
2026-07-25 20:44:19 -05:00
brooklyn!
1f6427b75e
Merge pull request #71665 from NousResearch/bb/deleted-worktree-fold
fix(project-tree): absorb deleted-worktree sessions into the parent home checkout
2026-07-25 20:42:43 -05:00
brooklyn!
a288fc341c
Merge pull request #71678 from NousResearch/bb/code-block-overflow
fix(desktop): keep code and diffs out of the tool overflow window
2026-07-25 20:42:22 -05:00
Brooklyn Nicholson
be1edef5ce fix(desktop): show the real effort on the composer pill
The pill hardcoded 'Med' whenever the surface had no explicit effort, so
a profile configured for high advertised a level the agent would not
use. Take the profile default as a fallback and drop the now-unused
shell.modelMenu.medium key across the locales.
2026-07-25 20:37:13 -05:00
Brooklyn Nicholson
e2122f22b8 fix(desktop): stop the model picker overriding the configured effort
Selecting a model applied 'preset.effort ?? medium', so a user running
agent.reasoning_effort: high was silently downgraded to medium on every
model switch and every new chat that inherited the pick. The Thinking
toggle and the effort radio group defaulted to the same literal.

Resolve all of them from the profile default instead, falling back to
DEFAULT_REASONING_EFFORT only when config has not loaded.
2026-07-25 20:37:09 -05:00
Brooklyn Nicholson
58f5a05655 fix(desktop): publish the profile default reasoning effort to a store
The composer only seeded effort from agent.reasoning_effort when it was
about to reseed the whole selection, which a sticky manual model pick
skips. The configured value was read and discarded, leaving every other
surface with no way to resolve the profile default.

Mirror it into $defaultReasoningEffort on every config load, independent
of the composer reseed, so the manual pick stays sticky without hiding
the default from the surfaces that need it.
2026-07-25 20:37:06 -05:00
Brooklyn Nicholson
7f2971039a fix(desktop): keep code and diffs out of the tool overflow window
A run of 3+ adjacent tool calls collapses into the 6.75rem
`.tool-group-scroll` window. That is right for status rows, but a patch
or execute_code row's body is a code block the user reads, and the
window squeezed it to a ~2-row viewport behind a fade mask.

The CSS break-out (`:has([data-tool-row][data-tool-open])`) already
lifts the cap once a row is expanded, which is why most tools are safe
to bound. It can't help here: the user has to notice the clipped block
and expand it before the code is legible.

Extend the existing UNBOUNDABLE_TOOLS opt-out to the code-bearing tools
behind an `isUnboundableTool` predicate, deriving the file-edit names
from `isFileEditTool` so a newly supported edit tool can't be exempt in
one place and clipped in the other. `terminal` stays boundable: console
output is a log tail whose last lines are the ones that matter, which
is exactly what the window pins.

Verified in Chrome against the app's compiled CSS: a 3-call run
carrying a 520px diff rendered at 108px before, full height after.
2026-07-25 20:35:01 -05:00
Brooklyn Nicholson
1e067ec0b5 test(project-tree): cover deleted-worktree fold into parent trunk
Assert the dangling-cwd session joins the parent's main lane and that no lane
keyed by the deleted worktree path survives.
2026-07-25 20:31:17 -05:00
Brooklyn Nicholson
47c95130f7 fix(project-tree): absorb deleted-worktree sessions into the parent home checkout
A linked worktree at <repo>-<suffix> that has been deleted leaves its
sessions with a dangling cwd: the git probe fails and no git_repo_root was
persisted, so the path-only heuristic promoted each one to its own
standalone project. Every abandoned worktree added another phantom entry to
the sidebar, and they accumulate indefinitely.

Recover the parent by trimming one -<segment> at a time off the basename and
returning the first sibling that resolves. A deleted worktree has no checkout
to return to, so its sessions land in the parent's trunk lane rather than a
lane keyed by the dead path.

Live worktrees are unaffected — they resolve through the git probe and keep
their own lane.
2026-07-25 20:31:17 -05:00
Brooklyn Nicholson
bfc8e3b00d fix(desktop): give ⌘T session tabs a live gateway so slash completions load
`useGatewayRequest` only exposed the gateway through a ref that its own
subscription effect fills in, so a component reading it during render saw
null on mount. Session tiles — what ⌘T and the tab-strip "+" open — did
exactly that and passed `gateway={null}` down to ChatBar. The slash
adapter is disabled without a gateway, so a new tab had no completions at
all while ⌘N, which resolves its gateway through a state-driven memo,
worked fine. Nothing re-rendered the tile afterwards unless the connection
state happened to flip, so it never recovered.

Return the `$gateway` atom as a reactive value alongside the ref and use
it wherever the gateway is needed as a render-time value. The ref stays
for `requestGateway`, which wants a stable identity. The model picker,
model visibility, and settings overlays read the same ref during render,
so they're moved over too.
2026-07-25 20:30:55 -05:00
Brooklyn Nicholson
58d805302a fix(desktop): let a slash command be typed anywhere in the prompt
The `/` trigger was anchored strictly at position 0, so the completion
popover only opened when the slash was the first character. Typing
`please run /clean` mid-message produced nothing to pick, which put every
skill out of reach unless the prompt started with it.

Position 0 and mid-message are genuinely two different things, so detect
them separately. At position 0 a slash is a command invocation and keeps
its arg completion (`/personality alic`). After whitespace it's an inline
reference inside prose, so it completes as a single token and never
expands into an arg step. Both stay trailing-anchored, which keeps file
paths (`src/foo/bar`, `look at /usr/local/bin`) from matching.
2026-07-25 20:30:55 -05:00
hermes-seaeye[bot]
ba159d6fa9
fmt(js): npm run fix on merge (#71667)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-26 01:21:38 +00:00
teknium1
751c15f9eb lint(desktop): exempt benign ref writes flagged by the new atom-mirror guard
The guard matches any `.current` write inside a `useEffect`, so it also
flags refs that are not mirroring a reactive value. Seven such sites
landed on main after the rule was written:

  - wiring.tsx: one-shot request-seen sentinel
  - billing/plans-view.tsx: previous-confirming tracker for focus return
  - config-settings.tsx: autosave bookkeeping
  - gateway-settings.tsx: monotonic request-sequence counters
  - toolset-config-panel.tsx: mount flag + one-shot provider-choice claim
  - voice-provider-fields.tsx: one-shot config seed flag

None mirrors an atom/prop/state value, so none can produce the
one-render-lag stale read the rule exists to prevent. Each gets an
eslint-disable with the specific reason rather than widening the rule,
which would reopen the hole for real mirrors.

Verified the guard still catches the real antipattern: a probe file
mirroring `useStore($activeSessionId)` into a ref via useEffect is
reported. eslint over apps/desktop/src is 0 errors, and the 24 remaining
warnings match the pre-change baseline exactly.
2026-07-25 18:12:48 -07:00
ethernet
e026fd61d8 lint(desktop): ban atom-mirrored refs via no-restricted-syntax eslint rule
A ref synced from a nanostores atom via useEffect lags the atom by one
render. Callbacks that read the ref instead of the atom get stale values —
cancelRun sent session.interrupt to the wrong session (#66485), and
steerPrompt/restoreToMessage/editMessage had closure-priority stale reads.

The rule catches the three shapes of this antipattern:
  - useEffect(() => { ref.current = value }, [value])
  - useEffect(() => { ref.current = value; ... }, [value])
  - useEffect(() => { setMutableRef(ref, value) }, [value])

All 79 existing hits get eslint-disable-next-line with a comment. The
legitimate ref writes (DOM instances, mount flags, request tokens,
prev-value tracking, prop mirrors) stay suppressed; the 11 real bug
sites will be fixed in the next commit so their suppressions can be
removed.

Rule is scoped to apps/desktop only — ui-tui and tests-js don't use
nanostores and don't have this pattern.
2026-07-25 18:12:48 -07:00
Teknium
72de75c0ab feat(curator): surface unmanaged skills and add curator adopt
`hermes curator status` reported only the skills it manages, staying silent
about curation-eligible skills it can never touch. On a 237-skill library that
meant 112 skills were invisible to every automatic transition with no signal
anywhere — the curator looked broken when it was working as designed.

A skill becomes curator-managed only when `created_by: agent` lands on its
usage record, and only the background review fork writes that marker. Two
populations therefore never qualify: records written before the marker existed
(no key at all, authorship unknowable) and every foreground
`skill_manage(create)` (unset by design — those skills belong to the user).

- skill_usage: `list_unmanaged_skill_names()` / `unmanaged_report()` enumerate
  the blind spot, tagging each row with `has_provenance_key` so the two causes
  are distinguishable. `adopt_skill()` writes the marker on user declaration
  and refuses bundled, hub-installed, external, and protected built-ins.
  Adoption never resets the inactivity clock.
- curator CLI: status prints an `unmanaged (no provenance marker)` block on
  BOTH the managed and no-managed-skills paths; new `adopt` verb takes names or
  `--all-unmanaged`, with `--dry-run` and a confirmation prompt on bulk.
- skills_sync: `_backfill_optional_provenance()` matched candidates by
  repo-derived path only, so a skill installed at `mlops/chroma` that upstream
  later moved to `mlops/vector-databases/chroma` was skipped forever and
  `hermes skills repair-optional` could not fix it. Falls back to an
  unambiguous name match, still gated on identical content, and records the
  ACTUAL install path.

Provenance stays a declaration, never an inference: a high patch count proves
the agent MAINTAINS a skill, not that it authored one, since Hermes edits
user-written skills on the user's behalf routinely. An "looks agent-made"
heuristic would eventually archive hand-written work.

Validation: 347 targeted tests pass. Each new test verified via sabotage run
(revert the fix, confirm the test goes red) — one initially passed for the
wrong reason because the fixture pinned `prune_builtins` off, masking the guard
under test; fixed to force the shipped default on.
2026-07-25 18:10:24 -07:00
webtecnica
b9fedab47a fix: curator labels bundled skills as agent-created (#64393) 2026-07-25 18:10:24 -07:00
teknium1
b6accee0d7 fix(acp): pin the session cwd for slash-command handlers too
Slash commands run on the event-loop thread, outside the per-turn
contextvars.copy_context() that pins the session cwd for the agent call.
/compress reaches agent._build_system_prompt(), whose "Current working
directory" line comes from resolve_agent_cwd() — so an unpinned handler
rebuilt the prompt against the Hermes install tree and PERSISTED it as
the session's cached prompt, re-poisoning every later turn even though
the turn itself is now pinned.

Pin inside a fresh context copy so the write cannot leak into other
concurrent ACP sessions on the shared loop and needs no teardown.
2026-07-25 18:06:32 -07:00
Steve Darlow
cca2a2fc8d fix(acp): pin the session cwd for the turn
An ACP session registered the client's cwd for the *tools*
(`_register_task_cwd` -> `register_task_env_overrides`) but never pinned it
for the *prompt*. `agent/prompt_builder.py` reports
`Current working directory: {resolve_agent_cwd()}`, and `resolve_agent_cwd()`
reads the `_SESSION_CWD` contextvar, which ACP left unset — so it fell back
to `TERMINAL_CWD` / the launch dir.

The system prompt therefore advertised one root (commonly
`~/.hermes/workspace`, or the install tree) while the tools were rooted at the
editor's project. When the model emitted a *relative* path the tools resolved
it correctly; when it emitted an *absolute* path built from the advertised
root, the write landed outside the client's workspace and the turn still
reported success.

Observed in Zed/Buzz-shaped usage: the first prompt in a fresh workspace
creates the file under `~/.hermes/workspace/` and answers "Done." The client's
directory is untouched. Later sessions on the same cwd appear to work once a
session cwd record exists, which makes it look intermittent.

`_run_agent` already calls `set_session_vars(session_key=session_id)` inside
its `contextvars.copy_context()`, and that helper's `cwd` argument exists to
"pin the logical working directory for this context". It simply was not
passed. `gateway/session_context.py` and `tui_gateway/server.py` both already
pass it; ACP was the only surface that did not.

Adds a regression test asserting that the resolved cwd *during the turn* is
the cwd the client passed to `session/new`. It fails without this change
(resolving the install tree instead of the client's project).
2026-07-25 18:06:32 -07:00
Ben Barclay
40eebc7d70
feat(relay): Phase 4 thread lifecycle — handoff threads, semantic renames, reply_to context, hello command manifest (#71624)
- RelayAdapter.create_handoff_thread → one op-gated thread_create op
  (Discord channel thread / Telegram forum topic / Slack named seed root);
  None fallback contract preserved for the handoff watcher.
- RelayAdapter.rename_thread → thread_rename with only_if_current_name
  no-clobber guard on the wire (connector enforces; Telegram guarded
  renames fail safe). The native semantic-rename lane
  (_is_discord_auto_thread_lane) lights over the relay via the
  connector-stamped auto_thread_created/auto_thread_initial_name markers
  parsed onto SessionSource in _event_from_wire.
- reply_to {text, author, is_own} wire parse onto the SAME MessageEvent
  reply-context fields native adapters populate.
- gateway/relay/command_manifest.py: the gateway-declared slash-command
  manifest (native Discord tree mirror) sent on the DISCORD hello; the
  connector reconciles Discord's global registration (additive field,
  older connectors ignore).
- Contract doc §OutboundAction ops + Phase 4 semantics sections.
- 12 new tests (tests/gateway/relay/test_relay_threads.py); relay suite
  266 passed.
2026-07-26 10:26:20 +10:00
teknium1
1161cc0b53 fix(managed-uv): don't retry patches at or below the installed version
The retry loop skipped only the current version, so on a uv whose download
catalog is stale it walked backwards through older patches. In #71250 the
newest indexed 3.11 is 3.11.14 — the installed version — so all five retries
went to 3.11.13..3.11.9, each a real download+install+probe+delete cycle that
the existing downgrade guard was always going to reject, before failing anyway.

Only newer patches can carry the SQLite fix. Skipping <= current makes the
stale-catalog case cost one attempt instead of six, and still finds 3.11.15
on the first retry when the catalog knows about it.
2026-07-25 16:42:49 -07:00
ygd58
866cdce209 fix(managed-uv): retry with explicit patch when bare-minor SQLite repair still resolves vulnerable
Fixes #71250.

`hermes update` could not repair the embedded Python runtime's
vulnerable SQLite (WAL-reset bug range 3.7.0-3.51.2, except backports
3.50.7/3.44.6) on installs where `uv python install <minor>` (bare
request, e.g. "3.11") resolves to an older cached/indexed patch that
still links a vulnerable SQLite build, even though a newer
non-vulnerable patch on the same minor line is available and known to
uv. The smoke test correctly rejected the vulnerable candidate every
time, but the provisioner gave up immediately after one attempt,
leaving the repair permanently stuck in the same failing loop on every
`hermes update` run.

Implements option D from the issue (query the index, retry with an
explicit newer patch), which the reporter identified as cleanest:

- New `_list_available_patches()`: runs `uv python list <minor>
  --all-versions --only-downloads --output-format json --no-config`,
  filters to cpython/default-variant entries (excluding pypy/graalpy),
  and returns known patch versions newest-first. Fails safe (returns
  []) on any network/parse error.
- Refactored the single install+find+probe cycle out of
  `_install_safe_python_generation()` into `_attempt_install_generation()`,
  reusable per attempt with its own generation directory (so a
  rejected candidate's files are fully cleaned up before the next
  attempt, matching the existing --reinstall semantics).
- `_install_safe_python_generation()` still tries the bare minor-line
  request first (preserves the original comment's rationale: for a
  given exact patch, python-build-standalone may have no artifact with
  fixed SQLite at all). If that resolves vulnerable, it now queries
  `_list_available_patches()` and retries with explicit newer patches,
  newest-first, bounded to `_MAX_PATCH_RETRIES` (5) attempts -- each
  attempt is a real download+install+probe cycle, so the cap keeps
  worst-case repair time bounded.

Sanity-checked `_list_available_patches()` against the real `uv`
binary (0.11.7) in this environment: correctly parses live `uv python
list --all-versions --output-format json 3.11` output, returns 14
patches sorted newest-first starting at 3.11.15.

9/9 new tests pass (retry succeeds with a newer patch, exhausts
gracefully when every known patch is vulnerable, empty patch list
degrades to None without crashing, retry count is bounded, plus direct
JSON-parsing unit tests for realistic/malformed/empty uv output);
47/47 in the full tests/hermes_cli/test_managed_uv.py file (including
the 3 pre-existing tests for the original bare-minor success path,
confirming no regression there).
2026-07-25 16:42:49 -07:00
teknium1
ec2a0f8c1e fix(sessions): point failed in-place repair at offline recovery
A failed `hermes sessions repair` printed "keep state.db and the backup"
and stopped, so a user whose sessions had vanished had no way to discover
that a non-destructive recovery path exists — the reported dead end.

The failure branch now names the next command, read-only step first, seeded
with the backup path it just preserved. Covered by a CLI-surface regression
test that drives the real subprocess against an unrepairable database.
2026-07-25 16:42:31 -07:00
Gille
a9b8128bcb fix(sessions): add offline state database recovery 2026-07-25 16:42:31 -07:00
teknium1
064b6e40c5 fix(image-gen): stop reporting the Codex tool_choice 400 as an account limit
The Codex image backend rejected our own request shape for every account, and
we then translated that rejection into "Image generation is not enabled for the
current Codex account. Switch the image provider to OpenAI API key, FAL, or
xAI." — telling every affected user to abandon a provider that had never
actually been tried. That message is why this reads as a setup failure rather
than a bug: the wire error was replaced with a confident, wrong diagnosis.

Removes the classifier and its exception, so any HTTP failure surfaces
verbatim. The paired request-shape fix (previous commit) is what makes the
400 stop happening; this commit makes the next one diagnosable.

Also fixes error-body truncation: bodies were head-truncated at 500 chars, and
Codex error payloads can carry hundreds of bytes of leading metadata, so the
user got a wall of padding and no message. _summarize_error_body() prefers the
parsed error.message and falls back to a truncated raw body.

Docs: drop the unqualified image-to-image claim for the Codex backend and note
that the hosted tool call cannot be forced, so it is best-effort.

Verified E2E against a local fake Codex backend: success path writes a real PNG
with no tool_choice on the wire; the 400 path now returns api_error carrying
"Tool choice 'image_generation' not found in 'tools' parameter" (148 chars)
instead of the entitlement message. Sabotage run confirms all 4 regression
tests fail when the old behavior is restored.

Refs #19505, #49008, #31335.
2026-07-25 16:41:57 -07:00
Tranquil-Flow
2bfe9fabbc fix(image_gen/codex): remove unsupported tool_choice from request payload (#19505)
The chatgpt.com/backend-api/codex backend 400s on every tool_choice
shape for the hosted image_generation tool — it looks up tool_choice as
a function name and never recognizes hosted-tool entries. Removing the
field from _build_responses_payload() lets the host model decide; the
instructions field nudges it toward the tool.

Salvaged from PR #19979 (originally targeted the old
client.responses.stream call, which no longer exists on upstream/main;
the live request now flows through _build_responses_payload + httpx in
_collect_image_b64).
2026-07-25 16:41:57 -07:00
teknium1
776c43befe perf(cli): cut hermes -w startup from ~14s to ~1.8s by parallelizing + caching the worktree prune
`_prune_stale_worktrees` runs synchronously before the banner on every
`hermes -w` launch and shells out to git several times per candidate
worktree. On a repo with dozens of accumulated worktrees this dominated
startup: measured 18.5s of a 20.6s cold start, 11.6s on warm repeats.

The `git cherry` patch-equivalence probe was the single largest cost
(9.4s of 11.5s across 24 trees). It is also pure waste on repeat runs: a
tree preserved because it holds unpushed work is re-diff-hashed on every
launch, forever, always reaching the same verdict. On this repo 19 of 24
aged trees were unreapable, so ~11s per launch bought zero reaps.

Two changes, both verdict-preserving:

- Split the loop into a stat-only age filter, a parallel read-only
  classification phase (thread pool, bounded to min(8, cpu_count)), and
  a serial mutation phase. Only reads are concurrent; unlock/remove/
  branch -D stay ordered, so log output and removal order are unchanged.
  A pool failure falls back to serial rather than blocking startup.
- Memoize `git cherry` verdicts to
  $HERMES_HOME/cache/worktree_merge_verdicts.json, keyed on the exact
  `(base_sha, head_sha, max_ahead)` range the verdict was computed from.
  Because that key is the complete input to the git call, a cache hit is
  identical to recomputation by construction: if either ref moves, the
  key changes and real git runs again. Bounded to 1000 entries, written
  atomically, and a corrupt/hand-edited cache degrades to recomputation.

Measured on a 44-worktree repo (24 aged candidates):

  _prune_stale_worktrees   before 11.5s   after 2.05s cold / 0.42s warm
  hermes -w to banner      before 13.9s   after 1.79s

Work-preservation is unchanged: all 44 worktrees survived, and every
dirty/unpushed/live-locked guard still fires. Verified the 24 real-tree
verdicts are byte-identical across serial, cold-cache, and warm-cache
runs.

Tests: 75/75 in tests/cli/test_worktree.py (67 existing + 8 new). The
new cache tests were sabotage-verified — swapping the exact-sha key for a
naive path-only key makes two of them fail by deleting a worktree that
had gained unmerged work, which is the data-loss case the key prevents.
2026-07-25 16:41:52 -07:00
hermes-seaeye[bot]
44480617ff
fmt(js): npm run fix on merge (#71638)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-25 23:26:29 +00:00
hermes-seaeye[bot]
2ba1e028e4
fmt(js): npm run fix on merge (#71634)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-25 23:18:02 +00:00
SHL0MS
6b26b409cf fix(desktop): /goal arg stays editable, kickoff queues when busy, slash header stops echoing long args
Four symptoms from the same /goal flow on desktop:

- Typing '/goal <text>' sealed the command into a directive chip on
  Space because /goal was registered without args:true, so the goal
  prose rendered awkwardly after a pill. The registry row now matches
  /personality and /tools: the arg stays editable text.

- The slash status header echoed the ENTIRE invocation ('slash:/goal
  <whole goal prose>') in mono, immediately above the backend notice
  that repeats the goal text again, and the kickoff user bubble that
  repeats it a third time. The header now carries just the command
  token (slash:/goal).

- When the session was busy, handleDispatch rendered 'session busy'
  and dropped the dispatch message. For /goal that message is the
  kickoff prompt, and the backend has ALREADY set the goal by then —
  the goal existed but the agent never heard about it, and later turns
  looked goal-unaware (#63352). The busy path now queues the kickoff
  on the composer queue: it sends on settle and is visible/editable in
  the queue panel meanwhile. Falls back to the old message if the
  queue rejects the entry.

- A slash command issued on a fresh draft created the backend session
  with no preview, so the sidebar row sat as 'Untitled session' —
  and when the kickoff was dropped, auto-title never fired either
  (it needs a completed user->assistant exchange). ensureSessionId now
  seeds the preview with the typed command.

Tests: registry row contract, busy-path queueing (kickoff neither
sends mid-turn nor vanishes), and the header-token assertion.
2026-07-25 16:09:38 -07:00
xxxigm
fe3dd9106a
fix(update): stop WoA integrity gate rejecting ARM64 after IsWow64Process2 HANDLE truncation (#71381)
* fix(update): bind IsWow64Process2 HANDLE and fall back to GetNativeSystemInfo

The #71218 OS-native probe still rejected correct ARM64 Desktop rebuilds on
Windows-on-ARM when ctypes truncated GetCurrentProcess()'s pseudo-handle and
IsWow64Process2 failed with ERROR_INVALID_HANDLE, falling through to the
lying PROCESSOR_ARCHITECTURE=AMD64 value. Type the HANDLE correctly and use
GetNativeSystemInfo before the env-var fallback.

* test(update): cover WoA IsWow64Process2 handle failure and system-info fallback

Pin the residual #71218 shape where IsWow64Process2 returns FALSE, the env
arch lies as AMD64, and GetNativeSystemInfo must still report ARM64 so the
integrity gate accepts a correctly-built ARM64 Hermes.exe.
2026-07-25 19:04:03 -04:00