An agent told to make a worktree and work in it left the chat on the checkout it
started in — the sidebar kept the chat in the primary branch lane while every
later command ran in the worktree.
`_reconcile_session_cwd_from_terminal` refused whenever `explicit_cwd` was set,
but `session.create` sets that for ANY session whose cwd exists on disk, and the
desktop always sends a workspace. The guard that was meant to protect a
deliberately chosen workspace (#72776) therefore disabled the follow for the
whole desktop surface.
Key the pin on `cwd_pinned`, set only by the two moves the user drives:
`session.cwd.set` (the composer's folder picker) and `session.workspace.move`
(move-to-project). A cwd a chat merely STARTED in is not a pin, so the agent's
own worktree settle re-anchors the chat and the desktop moves it to that lane.
Desktop session rows are created on the first submit (_ensure_session_db_row),
after _hydrate_session_cwd has already run against a missing row, so no git
probe was ever claimed and git_branch/git_repo_root stayed NULL. The sidebar
then labelled the lane with the DEFAULT_BRANCH_LABEL "main" fallback, and "+"
ran `git switch main`, which fails on a master-trunk repo.
- _ensure_session_db_row schedules the generation-scoped git probe once per
live session when the row has a cwd but lacks git metadata.
- "+" on a main-checkout lane switches only when git lists the label as a
branch (laneSwitchTarget); otherwise the session opens on the current checkout.
Fixes#108694Fixes#108784
app.exit() does not fire will-quit, and every in-app relaunch (About's
Restart, the bundle-swap relaunch, the GPU/no-sandbox relaunches) exits
through exitAfterBackendShutdown. Kill the timed git trees there as well,
or a probe in flight at relaunch still ends up under PID 1.
With lazy fetch off, the path-limited rev-list walk fails on a tree:0
checkout as soon as an update moves more than one commit: it needs the
tree of every intermediate commit, and only the checked-out endpoints are
local. The probe then reported "not stale" exactly when the renderer is
most likely behind, on the install shape `hermes update` produces.
When the walk cannot run, compare the stamp and HEAD endpoints over the
runtime paths. Ancestry is already proven, so a differing diff means the
renderer predates runtime changes. The diff reads only the two endpoint
trees, offline. Measured on a real tree:0 clone of this repo: 45 ms, no
new packs. If the stamp's tree is missing too, git exits 128 and the probe
stays quiet as before.
Proposed by @Froraut on #123039, with field data.
Co-authored-by: Froraut <79215537+Froraut@users.noreply.github.com>
Timed execGit commands lead their own POSIX process group, so nothing
reached a probe still running when the app quit: the 5 s timer died with
the process and the group was left orphaned under PID 1 (#125243's
"survives restart"). execGit now tracks timed children, and will-quit
kills each tree (group SIGKILL on POSIX, taskkill /T /F on Windows).
On Windows a root that already exited has no tree left for taskkill /T to
walk and its pid may be reused, so the timeout path skips taskkill there.
The error/close handlers no longer repeat finish()'s clearTimeout.
Quit-time cleanup was first proposed in #125861.
Co-authored-by: chelsealong <chelsealong@126.com>
The bundle-skew probe was runGit's only caller. With the probe on execGit
(timeout, process-tree reap, GIT_NO_LAZY_FETCH), runGit is dead: an untimed,
untracked spawn nothing should reach for again.
Its spawn-failure path goes with it. describeGitSpawnFailure and the
GIT_UNUSABLE marker had exactly one producer, runGit, and the only runGit
caller swallowed every error, so the hermes:updates:check handler's
git-unusable branch could not fire; nothing in the renderer reads it.
A real `--filter=tree:0` clone over file:// runs the probe and asserts the
pack directory is unchanged. Without GIT_NO_LAZY_FETCH the same probe writes a
new promisor pack (4 more files under objects/pack).
Cherry-picked from the test half of #123039 (fc5ea944bd); the env change it
carried is already in the previous commit.
Coalesce concurrent version polls, cache results briefly, and skip checks
while an updater owns the local runtime. Use the existing execGit runner
with a 5-second timeout and no lazy fetch; reap its process group or Windows
process tree so Python-hosted git and promisor children cannot accumulate.
Add concurrency/backoff coverage and a live descendant-process regression.
Related to NousResearch/hermes-agent#125932.
(cherry picked from commit 633d7de6cd68164d2293c47f8e37170debabeaed)
FIFO session eviction in tui_gateway/event_replay.py deleted the session's
_replay_next_seq, so a session revisited after eviction restarted at seq 1,
under every client watermark, and session.events.since returned nothing new.
Keep the seq counter for the process epoch and raise _replay_evicted_through
to the last stamped seq, so an old watermark gets truncated: true instead of
an empty, complete-looking reply.
The client half (acting on truncated: true) is
https://github.com/NousResearch/hermes-agent/pull/126988.
Both rows are already curated lists (Nous = curated manifest + the Portal's
recommendations, OpenRouter = curated ∩ live ∩ tools), and the free tier
sits last by design. The gateway /model picker and the TUI/Desktop payload
ask for max_models=50, so the tail was sliced off: on main the Nous row had
58 entries (8 Portal-recommended free models hidden, space-bunny-alpha
among them) and OpenRouter had 54-55 (the whole free block hidden). Adding a
catalog entry alone only swapped which free model fell off the end.
- nous and openrouter join _UNCAPPED_PICKER_PROVIDERS; list_picker_providers
caps its OpenRouter override through the same _cap_models.
- _cap_models honours max_models=0 for uncapped rows too (slug-only
callers asked for no models and got a full uncapped catalog).
- The platform text fallback slices its own 5-model preview instead of
relying on the listing cap, so uncapped rows do not dump into chat.
Space Bunny Alpha is a live $0 stealth SKU on OpenRouter (tool support, 1M
context, image/video input) that the picker never offered: the OpenRouter row
is the curated manifest intersected with the live catalog, and the id was not
in the curated list, so the live entry was filtered out.
- models_catalog_static.py: id + "free, stealth model" description, and
membership in _OPENROUTER_ONLY (stealth SKU without the :free suffix, not
carried by Nous Portal)
- website/static/api/model-catalog.json: regenerated
(scripts/build_model_catalog.py, 63 -> 64 OpenRouter entries)
Fixes#122075
The packaged Desktop with no default project dir resolves its spawn cwd (and
TERMINAL_CWD) to $HOME, and SubdirectoryHintTracker's containment check is
then vacuously true for the whole home subtree: any AGENTS.md, CLAUDE.md or
.cursorrules under ~ is injected into tool results on first touch. The
tracker now treats a home (or container-home) working dir as a fallback, not
a project — hint discovery stays off until the session adopts a real
workspace, and _register_session_cwd re-anchors it there (same hook that
already updates agent.session_cwd). AIAgent init also prefers the session's
own cwd over the launch TERMINAL_CWD, so an agent built for a workspace
session scopes hints to it from the start. .archive joins the excluded dir
names (retired skills trees hold stale AGENTS.md copies).
Rebases the home guard from #77145 (idea + guard direction) onto the current
class shape; covers the install-tree shape #75922 targeted via the shared
home guard.
Fixes#76902
Ref: https://github.com/NousResearch/hermes-agent/issues/75839
Co-authored-by: Sahil-SS9 <218421507+Sahil-SS9@users.noreply.github.com>
Two prompts accepted busy, the first drains, then the backend dies before
the second drains: the reopen retires the second's still-marked row while
the drained turn's healed row stays active. The never-run prompt's copies
survive inactive (durable history is never deleted).
A prompt accepted while the session's agent is busy writes its user row at
accept time; only the in-memory queue drain ever retires that row. A backend
restart discards the queue with nothing left to run, so the row stayed active
and the next resume's alternation repair glued the never-run prompt into the
previous turn's user message.
Mark the accept-time row (display_metadata._queued_prompt) and deactivate
still-marked rows in reopen_session — the drain's replacement row is
unmarked, so still-marked after a restart is exactly never-drained. A
dispatched-then-interrupted turn's row is never marked (its in-process
discard is #123532's contract). Rows are deactivated, never deleted.
The top-level import was unguarded (its neighbours already had try/except
patterns), so a partial prompt_toolkit install — e.g. a stale venv with the
package present but a submodule broken — crashed every import of cli,
including tui_gateway.slash_worker, which every gateway session spawns.
The only use site is a 'with patch_stdout():' block, so fall back to
contextlib.nullcontext, mirroring agent/display.py's guarded
patch_stdout StdoutProxy import.
Fixes#96075
The cut printed before GitHub listed the dispatched run, so it almost always
said "the run is not listed yet". It now polls up to 30s for the run, and when
none lists it links the stable-release workflow page. The copy is shorter:
Release CI, the skipped-tests warning (bold on a tty), the draft link, and the
publish command on its own line.
git keeps an annotated tag's signature inside the message body, so
`git tag -l --format=%(contents)` returns the JSON record followed by an
armored block, and `json.loads` of the whole message raises "Extra data: line 2
column 1" at the armor's first byte. Claim tags, final receipts and build
receipts are created on a host whose git signs tags, so stable admission
(`python -m scripts.releases.stable admit`), ordered reconciliation
(`python -m scripts.releases.sequencer`) and tagless build receipts each
refused to read their own tag.
Add `versioning.tag_record()` as the one reader of a tag's record — the text
before the armor, unchanged for an unsigned tag or a detached signature — and
route every `%(contents)` read through it in stable.py, sequencer.py and
commit_build.py. commit_build imports it inside the function so its module
surface stays stdlib-only for the isolated checkout-admission step.
The reads inside the release tests use the same reader, and the one fixture
that creates a plain final tag says so explicitly, so the release suite passes
on a host whose git signs tags by default.
Four tests added after the clarify contract change still sent a top-level
choices/question request. The clarify handler reads only questions, so it
stored no card and the tests failed on main.
OpenAI's Ultrafast service tier (service_tier="ultrafast") is broadly
available for GPT-6 Astra in the API and Codex (Pro 500 / Enterprise),
billed at 6x Standard. GPT-6.1 Sol Ultrafast is announced as coming soon.
- One tier table: agent.fast_mode.parse_service_tier / STATIC_TIERS now
back the CLI, gateway and TUI config loaders (three hand-copied parsers).
- resolve_fast_mode_overrides(tier="ultrafast") sends the tier only for
Ultrafast models on api.openai.com / chatgpt.com; anything else gets
nothing rather than a silent swap to another paid tier.
- /fast ultrafast on CLI, gateway (typed + picker when supported) and TUI;
static-tier attach sites pass the tier through.
- Pricing: the response's SERVED service_tier is folded into usage (Codex
stream assembler keeps it); served ultrafast prices from a published
6x row (272K whole-request tier included). A request asking for
Ultrafast but served at default bills at Standard.
- i18n: 4 new keys in all 17 locales; option lists mention ultrafast.
- A selection that resolves to no entries (e.g. ":(exclude)*") is refused
before the pre-rollback snapshot. Checkout with an empty pathspec file
switches the store's HEAD instead of restoring nothing.
- Remove the nested repository with utils.rmtree_readonly so the
deleted-repo test runs on Git for Windows (read-only loose objects).
- Check out the entries resolved during inspection as literal paths,
instead of re-evaluating the pathspec after the pre-rollback snapshot
restages the index (attribute pathspecs could select an uncaptured
gitlink the inspection never saw).
- Put a multiline reason on one line before appending the nested-repo
note; listings read %s, which stops at the first blank line.
- Gate host-specific test rows with platforms() and decode raw-byte
filenames inside the test, so the file collects on Windows, passes on
macOS and is selected by the OS lanes.
A nested repository is stored as a gitlink, so its files are not in the
checkpoint. Say so when the checkpoint is taken: its message gains
"[nested git repos not captured: <paths>]", which every /rollback listing
(CLI, gateway, TUI, Desktop) already shows. The gitlink parse is shared
with the rollback guard.
rollback.list sent the TUI an always-empty message (it read a "message"
key the manager calls "reason"), so TUI and Desktop listings never showed
what a checkpoint was; map it from reason.
Co-authored-by: FroRaut <79215537+Froraut@users.noreply.github.com>
A one-component gitlink cannot catch separator or component-splitting
mistakes in the scoped restore guard. Pin refusal for the exact gitlink,
its ancestors and descendants in every spelling (including Windows
separators on Windows), and unrelated recovery beside it.
Co-authored-by: FroRaut <79215537+Froraut@users.noreply.github.com>
The scoped restore guard resolved the requested path through the live
filesystem, so a captured file later replaced by a symlink into a nested
repository was refused even though the checkpoint holds its bytes.
Normalize the selection lexically; resolved-path confinement stays in
_validate_file_path. A recorded gitlink replaced by an unrelated symlink
is still refused.
Resolve selective restores against a disposable index before any checkpoint mutation, preserving unrelated-file recovery and refusing uncaptured gitlinks. Keep NUL-delimited filenames lossless, including surrogateescaped POSIX bytes. Add real-Git regression coverage and document the capture limitation.
Fixes#127073
Salvages #127077
Co-authored-by: Yuan Li <dskwelmcy@163.com>
OpenAI released GPT-6.1 Sol on 2026-09-29. It is live on OpenRouter
(openai/gpt-6.1-sol, -pro) and the direct API; not yet on Nous Portal
or on Codex OAuth for our test account.
- Catalog: OPENROUTER_MODELS + openai-api picker; kept off the nous
list via _OPENROUTER_ONLY until the Portal serves it. Manifest regenerated.
- Context: 1.05M direct API, 272K Codex fallback; 272K compaction
auto-raise on the Codex route.
- Pricing: $2/$10, cached input $0.10 (5%, not 6 Sol's 10%), cache write
$2.50, 2x/1.5x above 272K; -pro aliased to the base row.
- Reasoning: none/minimal return 400 on 6.1 Sol (unlike 6 Sol), so it uses
Astra's low..max ladder without Astra's account gating. OpenRouter's
catalog wrongly lists 'none' for it, so the OpenRouter profile omits a
disable when the OpenAI ladder has no 'none'.
Post-turn rehydrates can race the gateway commit: the refreshed page shares the anchor but ends before the just-settled reply. preserveLocalPendingTurnMessages only protected stream-id rows, so a reply that settled onto a non-stream id (interim id, appended assistant-<ts> bubble) was dropped after it already painted. Extend the #70209 only-copy rule to the single newest settled, non-interim, unpersisted assistant row when the refreshed page carries no identical text.
That row is skipped when the refreshed page brings its own assistant row for the same reply ordinal: a rotated or compacted page supersedes it, so keeping the cached copy would render the pre-compression answer next to its replacement.
Closes#121613
* fix(security): the first tirith scan no longer waits on its download
A process that never ran the CLI startup install (desktop serve, the
gateway, a newly routed profile) claimed the install on its first scan and
ran pm.ensure("tirith") inline. The first terminal call then waited on the
PM worker, the runtime toolchain and the tirith download before it ran.
The cold scan now starts the same background install as startup and scans
with the default path, so the call fails open at once like any scan during
a startup download.
* fix(security): a scan during the tirith download never trips the breaker
The previous commit let a cold scan run while the background install was
still downloading. That scan spawned the bare default "tirith", got
FileNotFoundError and counted it as a crash. Three terminal calls during
the download opened the circuit breaker, and every scan then returned
allow unscanned for 300 s, even after the download finished. With
tirith_fail_open: false the first three commands were blocked and the
breaker then let commands run unscanned.
A scan that finds the default binary missing while this home's install
thread is alive no longer spawns or counts a crash. Fail-open allows the
command at once ("tirith installing"). Fail-closed waits for the install,
as the inline install did before, and then scans with the real binary.
So only the fail-open case skips the scan during a download; the earlier
commit's "fails open at once" holds only for fail_open.
The PM consumer test now joins the background install before it reads
the recorded download requests.
The layout-tree renderer (63a9bde77) introduced PaneTabLabel with a
text-[9px] override that contradicts the parent PaneTab's baseline of
text-[0.6875rem] (11px). The old ChatHeader/TitleMenuTrigger used 0.75rem
(12px), making 9px a clear regression.
Drop the explicit size class from the label span so it inherits 0.6875rem
from the parent — consistent typographic hierarchy, still a meaningful
improvement over 9px.
An empty bridge answer produced one string that blamed a closed tab
('The action timed out, or no GUI window answered. Open a page with
open_preview first.') even when the pane was open on an app older than
this backend — two cases with different remedies. drive_preview /
annotate_preview now lead with 'no preview tab is open' and name the
older-app possibility; read_preview / read_terminal name both the
nothing-open and bridge-timeout cases and the update-the-app remedy.
Refs #94272
Refs #95104
Refs #119333
Co-authored-by: lesseradmin <veeman961@gmail.com>
drive_preview / annotate_preview blocked the full 45s deadline on every
call when nothing could answer preview.act — an app build older than the
tools (the renderer ships in the desktop bundle, the backend offers the
tool), or a session no attached window hosts — and the tool then blamed a
closed tab. Port PR 94300's fail-fast ladder onto the JSON-RPC
server-requests path, mirroring _tour_request: first action per session
gets a 10s probe deadline; unanswered → bridge marked unavailable for
that session with an actionable update-the-app error, and later actions
short-circuit instead of stacking waits. Unlike tour support, a preview
pane can open later in the session, so an unanswered probe only condemns
the bridge for a cooldown window, with a single in-flight reprobe per
session; concurrent callers fail fast. Once answered, the full deadline
is back and a transient miss never disables the bridge. A window-owned
request that every window declines settles as a non-empty answer
(main e33f1a0faf): the refusal passes through unchanged.
Fixes#94272
Co-authored-by: lesseradmin <veeman961@gmail.com>