Commit Graph

166 Commits

Author SHA1 Message Date
shali10
40523600b0 fix(sessions): refuse to delete a session row a live turn still owns (#123583)
Refactor entry-side deletion refusal to execute in-transaction via
`_write_guards_reject(conn, sid)` (#123583), per maintainer review:

- Underlying `delete_session` and `delete_sessions` now accept an opt-in
  kwarg `exclude_active_write_guards=True` running inside `_do` write
  transaction, eliminating the race condition where a turn acquires the lease
  between check and delete.
- Raises `SessionActiveWriteGuardError` when refusing single delete, leaving
  the row untouched; `delete_sessions` atomically skips active rows.
- Checks both active turn leases and compression locks via the existing
  reclaim-aware `_write_guards_reject` helper.
- Covers all user-facing delete sinks:
  * Web `DELETE /api/sessions/{id}` -> 409 Conflict
  * Web `POST /api/sessions/bulk-delete` -> skips active rows
  * Web / CLI `prune` -> passes `exclude_active_write_guards=True` so lineage
    parents of active conversations are not pruned
  * API Server `DELETE /api/sessions/{id}` -> 409 session_active_turn
  * CLI `hermes sessions delete` & `export --delete-after-verified` -> exits 1
  * CLI browse picker -> refuses active delete
  * TUI Gateway `session.delete` -> 4023 error
- Conforms to rubric with 2 targeted invariant tests in
  `tests/hermes_state/test_delete_session_write_guards.py`.
- Updates user guide and web dashboard docs for 409 / exit 1.

(cherry picked from commit 2c037a7a79dc211b49bacc72e3140951ccf900cf)
2026-09-27 20:49:04 +05:30
JoaoMarcos44
2a977be999 fix(status): render the session profile home
Let the shared status field builder accept an explicit owning home and have the
multiplexed TUI/Desktop session.status path pass its session profile_home.
Unscoped CLI/gateway callers keep the historical process-home fallback.

Add focused coverage for a secondary-profile session and for the launch-profile
fallback.

Fixes #124500.
2026-09-27 07:43:23 -05:00
Xipong
e0bc2c1ce9 fix(desktop): retain access to past tasks after turns 2026-09-27 07:03:12 -05:00
Hermes Agent
4153869e6e fix(tui-gateway): tolerate stores without db_path in session.list subagent scope 2026-09-26 20:40:40 -05:00
Hermes Agent
2e43498413 fix(sessions): add sessions.show_subagents to list delegate runs 2026-09-26 20:40:40 -05:00
brooklyn!
5eeee479f8 fix(tui_gateway): resume wake detector after session.interrupt
session.interrupt silenced streaming TTS and left a voice-owned wake
pause in place, so the next wake phrase was ignored. Re-arm the caller
or voice-capture lease after an accepted interrupt, and after an error
that already cut TTS. A hosted-task not_interrupted mismatch leaves the
pause alone.

Fixes #117747
2026-09-26 20:23:50 -05:00
kshitijk4poor
a0fe806c46 fix(tui): ssh cwd shape and ~ everywhere; named ssh never inherits the launch cwd
Round-4 pre-arm review warnings (ssh-only gaps, no local/docker change):

- A named ssh profile's cwd chain falls back to "~" before the launch
  profile's host cwd, so a fresh or resumed TUI/dashboard session never
  runs the remote shell in a host path.
- An ssh launch profile keeps a "~"/"~/x" cwd remote in session.create /
  _completion_cwd (before main's host fast path), matching session.cwd.set
  and workspace.move.
- One _is_remote_cwd_shape (reusing _is_ssh_remote_tilde_cwd) gates every
  remote path: session.create no longer marks a relative remote path
  explicit, and _completion_cwd returns the profile's own cwd (or ~) for
  one, matching _workspace_cwd's rejection.

Live A/B over 6 launch modes x 7 profiles x 7 cwd shapes: every local,
docker and backendless cell is identical to origin/main.
2026-09-26 21:40:44 +05:30
kshitijk4poor
fd1ff96c82 fix(tui): ssh "~" stays remote; docker named profiles keep main's task cwd
Round-2 pre-arm gate findings:

- A named ssh profile's cwd is checked before any host expansion:
  _profile_workspace_cwd tries the declared remote cwd first, and
  _completion_cwd returns an ssh-bound path raw before expanduser/isdir.
  A `terminal.cwd: ~` (or ~/x) no longer resolves to THIS host's home
  and gets pinned as the remote workspace.
- _terminal_task_cwd_with_source switches to ssh only for a named ssh
  profile; other named backends keep main's process-backend branch, so a
  named docker profile under a local launch keeps its "session" cwd
  source (docker isolation mounts from it).
- Launch-profile heal/reconcile keeps main's env check and only adds the
  config-says-ssh case, so a docker-in-config launch still heals dead
  host worktrees as on main.
- _completion_cwd does not fail an explicit client cwd for a deleted
  profile (main never resolved the profile on that path).
- One _workspace_cwd(profile_home, raw) validates a picked workspace for
  both _set_session_cwd and session.workspace.move.
- The profile-policy helpers live beside their callers in
  session_workdir; reuse hermes_cli.config._is_ssh_remote_tilde_cwd.
- session.create resolves the backend only when a cwd was sent.
- Tests write a real profile config.yaml instead of stubbing the backend
  helper; pin the "~" case.
2026-09-26 21:40:44 +05:30
kshitijk4poor
f4470b76fe fix(tui): keep docker/other backends on host checks; drop per-turn row write
Pre-arm gate findings on the salvage stack:

- The "cwd lives on another host" exemption is ssh-only (_cwd_is_remote).
  Docker and the other backends mount or copy HOST paths: _set_session_cwd
  keeps the host isdir check and always calls cleanup_vm, so a docker
  session moving workspaces gets a fresh container with the new mount
  again (the non-local early return skipped it).
- _ensure_session_db_row no longer force-writes the row cwd. It runs on
  every prompt, and each update_session_cwd bumps git_metadata_generation,
  which made an in-flight git-meta probe fail to publish. The eager row at
  session.create already lands the cwd before the agent's INSERT-OR-IGNORE.
- A named profile's backend and remote cwd come from the policy its turns
  actually run under (tools/terminal_scope.build_profile_terminal_scope:
  defaults <- .env <- config.yaml), so a .env-only TERMINAL_ENV=ssh
  counts; _profile_configured_cwd is back to main's body.
- An ssh profile's own terminal.cwd is also used when the client sends no
  cwd (_profile_workspace_cwd, shared with resume).
- session.workspace.move takes the backend from the live session's
  profile (the same one _set_session_cwd uses) and validates once.
- _hydrate_session_cwd resolves the backend outside _sessions_lock.
- _completion_cwd keeps main's host fast path; the backend is only
  resolved when the path is not a host dir.
- Delete the now-dead _is_local_terminal_backend; the cwd-follow fixture
  patches _effective_terminal_backend instead (its old patch was a no-op).
- Trim three tests that re-asserted main's local behaviour or duplicated
  the real-config tests.
2026-09-26 21:40:44 +05:30
kshitijk4poor
fc589b6898 fix(tui): one bound-profile backend for every workspace-cwd decision
Follow-up to the #105749 + #123903 salvage:

- A named profile without terminal.backend is local. Both contributor
  helpers fell back to the LAUNCH profile's backend, so a local profile
  opened from an ssh launch had its terminal.cwd treated as remote.
- _bound_terminal_backend() is the single resolver (create, completion,
  workspace.move, display heal, settle-follow, terminal tool). The
  terminal tool now reads it too, so an ssh profile under a local launch
  keeps /home/kali instead of the display heal persisting /home.
- _declared_remote_profile_cwd() only honours a profile that itself
  declares backend: ssh; the placeholder set is gone (the ~/absolute
  shape check already rejects ".", "auto", "cwd").
- One loader for a named profile's terminal section
  (_profile_terminal_cfg), shared with _profile_configured_cwd.
- Eager row at session.create and "row cwd = explicit" on hydrate apply
  to remote sessions only; local project drafts stay lazy and keep
  settle-following, as on main.
- config.get project forwards the pinned profile (and marks a picked
  path explicit) so the desktop gets the remote dir back; the renderer
  keeps adopting the server's normalized cwd for local users (WSL
  translation, abspath) instead of bypassing it.
- Dropped #123903's cwd_explicit-decides-intent change in session.create:
  it made every local desktop new chat in a project lose its workspace
  and AGENTS.md.
2026-09-26 21:40:44 +05:30
xxxigm
e8d86e7405 fix(tui): use a named SSH profile's working directory in the desktop app
The app showed the profile's terminal.cwd, but SSH sessions still ran in the
launch profile's directory because a remote path that does not exist on the
desktop host was discarded.

(cherry picked from commit e4f4a43ebc4408adb6ac37e8de1ee7ff414a9158)
2026-09-26 21:40:44 +05:30
Bergmann89
54e3691926 fix(session): resolve a project session's cwd from the bound profile's backend
A multiplexed gateway serves many profiles from one process; at session.create
HERMES_HOME is not yet rebound to the target profile, so the process-global
backend check reads the launch profile (usually local) for a session bound to an
ssh/docker profile. The local isdir gate then drops the session's remote project
cwd, and the sidebar/terminal fall back to Home / the profile's ~ dir. Read the
BOUND profile's terminal.backend and, when non-local, trust the remote path raw
across the whole cwd path:

- _completion_cwd / session.create explicit_cwd / _set_session_cwd / workspace-move
  and a session-aware _session_is_local_backend (no launch-process backend reads);
- don't heal a live remote cwd down to /home (env-OR-config backend check);
- persist a project session's row eagerly with its cwd, and force the cwd on after
  the AIAgent INSERT-OR-IGNORE, so the sidebar keeps it out of Home;
- mark a hydrated row cwd as explicit so the remote terminal uses it, not ~.

(cherry picked from commit 35511526c5633e28e275f231e1735c2394f6afec)
2026-09-26 21:40:44 +05:30
Austin Pickett
a6578fcaa5 fix(tui-gateway): queue busy-mode follow-ups while compression is in flight
A correction delivered to the provider mid-compression aborts the
compression (explicit_interrupt) - the user's follow-up kills the very
turn that would have answered it. The channel-side busy path already
demotes interrupt->queue for this exact reason (gateway/run_busy.py,
#56391); mirror that demotion for the local RPC path so TUI, Desktop and
classic chat share the Discord-gateway contract: prompt.submit busy
steer/interrupt and session.steer/session.redirect queue instead, and
the follow-up drains when compression finishes.

Fixes #61042
2026-09-25 16:03:54 -04:00
Hermes Agent
8a639e80fe fix(tui-gateway): log when a client model override beats the profile default at session.create
The desktop composer's manual model pick rides into session.create as a
per-session override and silently wins over model.default for every new
chat. Name the override and the profile default in agent.log so the
choice is diagnosable from logs alone (#107410).
2026-09-25 11:31:32 -05:00
Hermes Agent
c953b2a03e fix(tui): never auto-resume a source='unknown' guard placeholder
session.most_recent applied only the listing deny-list, so a
source='unknown' row minted by the update_token_counts guard (legacy
messages with no sessions row) could outrank the session the user
actually opened and get auto-resumed — the sidebar then highlighted one
chat while the pane streamed another (#54320).

Auto-resume now uses its own deny check: the listing deny-list plus
source='unknown'. Human-facing listings keep showing those rows (they
may be a real session awaiting repair) — only the pick-a-session-for-me
path skips them.

Supersedes https://github.com/NousResearch/hermes-agent/pull/54479 (whose
promotion/repair approach is now delivered on main by the
token-accounting guard's source-aware upsert; its remaining
most_recent deny is adopted here).

Fixes #54320

Co-authored-by: Dustin Persek <dustin.persek@protonmail.com>
2026-09-25 11:13:34 -05:00
ethernet
c70129205f Merge remote-tracking branch 'origin/main' into ethie/pm-clean 2026-09-24 05:10:12 -04:00
brooklyn!
ecacf3d0c9 fix(desktop): declare the branch methods in the typed gateway contract
Rebased onto main, where params are validated against tui_gateway/contracts
and unknown keys are rejected: the copy_parent_history / omit_messages flags
on session.create and session.branch now answered 4000. Keep both methods'
wire shape unchanged and give the two new methods their own contracts
(session.branch_stored, session.branch_whole) with messages_omitted results;
the handlers take the behaviour as keyword arguments, not wire flags.
2026-09-24 03:57:08 -05:00
Benjamin Brumbaugh
1c7ec93aea fix(desktop): preserve branch compatibility with older gateways 2026-09-24 03:57:08 -05:00
Benjamin Brumbaugh
44812b5e90 fix(desktop): branch long sessions without renderer transcript load 2026-09-24 03:57:08 -05:00
ethernet
cb7b18431a Merge remote-tracking branch 'origin/main' into ethie/pm-clean
# Conflicts:
#	apps/desktop/src/app/settings/connections-registry.tsx
#	scripts/install.ps1
#	scripts/install.sh
#	tests/hermes_cli/test_update_autostash.py
2026-09-24 03:48:31 -04:00
brooklyn!
0641ee1dcd fix(tui-gateway): never park an idle /steer on the agent
With no live turn, AIAgent.steer() still accepted the text and the gateway
answered 'Steer queued'. Nothing drained it until the next turn's pre-API
drain spliced it after the newest tool row, possibly one from an earlier
turn (misplaced, cache-breaking), or it was requeued.

- command.dispatch /steer: idle sends the text as the next turn with a
  notice (CLI parity) instead of calling agent.steer().
- session.steer: idle answers 'rejected' without touching the agent, so
  clients fall back to their next-turn queue.
- TUI /steer: a rejected steer is queued for the next turn, not dropped.

Fixes #64578
2026-09-24 02:40:39 -05:00
ethernet
d75d3fe15c Merge origin/main into ethie/pm-clean
Conflict resolutions and semantic fixups:

- tools/environments/base.py: main's hard-exit kill fence (kill a spawn the
  fence missed, deregister from _live_foreground in a finally) wrapped around
  pm-clean's output collector.
- pyproject.toml: pm-clean's marker list plus main's new `live` marker.
- hermes_cli/main.py: pm-clean runs startup recovery from hermes_bootstrap, so
  the old early-recovery block stays gone; main's interrupted-pull restore
  (auto-merged above it) runs right after bootstrap, as on main.
- hermes_cli/update_cmd.py: main's interrupted-pull marker now guards
  pm-clean's first tree mutation (release-tag detach, ff-only, or reconcile)
  and is cleared once git is done. The marker's target is the ref git actually
  moves to (a release tag, not always origin/<branch>), since the restore
  compares against it.
- hermes_cli/_early_recovery.py: restore `import subprocess`, which pm-clean
  had dropped and main's auto-merged restore needs (NameError on the first
  launch after a killed update; test_update_interrupted_pull red -> green).
- apps/desktop/src/i18n/{de,es,fr}.ts: main's new locales carry the full
  settings.about block; trim it to `updates` as pm-clean's type and the other
  overlays do (tsc: 27 errors -> 0).
- main's new e2e tests: `import yaml` -> hermes_yaml; wake-word import table
  names pyopen_wakeword (pm-clean's wake-openwakeword extra); the anthropic
  key-leak switch leg needs the SDK, and the api_server two-tenant test needs
  aiohttp, both PM runtime extras the test env does not carry.
2026-09-23 21:55:59 -04:00
Xipong
b066034eaf fix(desktop): preserve interim messages across history and tool boundaries (#107386)
* fix(desktop): preserve interim commentary across history and tool boundaries

* fix(desktop): preserve interim commentary across history and tool boundaries

* fix(desktop): keep public Codex commentary out of hydrated thinking

Project profile-scoped, sanitized display commentary and reasoning for REST and gateway history without changing persisted replay items. Preserve canonical finals across stream recovery and keep tool-delimited interim text through settlement.

---------

Co-authored-by: Xipong <217837358+Xipong@users.noreply.github.com>
2026-09-24 00:30:45 +00:00
teknium1
2ec129528d fix(sessions): a /branch child row carries the parent's system prompt (CLI + TUI/Desktop)
The branch copies the parent transcript byte-for-byte so its first turn can hit the warm
prefix cache, but the child row was created without a system prompt. The child's first
build then found nothing to restore and re-probed the workspace, rewriting the prompt at
byte 0 for any repo that moved since the parent's session start — the same rewrite this PR
removes for /compress and resume, on the one session transition it did not reach. It also
logged the "stored system prompt is null; investigate update_system_prompt" WARNING for
every branch.

Both branch writers now pass the parent's prompt into create_session: the CLI prefers the
running agent's cached bytes (what this process sends) and falls back to the parent row;
_persist_branch (TUI/Desktop, seeded and lazy) reads the parent row. With the row in place
the seeding in agent/system_prompt.py replays the snapshot on the branch's later rebuilds.

Tests: one per surface, red on the PR head (child["system_prompt"] is None), green here.
2026-09-23 17:06:48 -07:00
ethernet
97de4b4ef8 Merge origin/main into ethie/pm-clean
Conflict resolutions and semantic fixups:

- utils.py / hermes_yaml.py: main widened ruamel's round-trip emitter so a long
  double-quoted scalar is never folded after an escaped backslash. pm-clean builds
  every rt emitter through hermes_yaml.roundtrip_yaml(), so the width lives there
  (ROUNDTRIP_YAML_WIDTH moves with it); xai_retirement imports it from hermes_yaml.
- hermes_cli/banner.py: keep pm-clean's removal of the banner update check. Main's
  GIT_NO_LAZY_FETCH fix for it applies to its replacement, source_check: every
  read-only probe (source_git_env) now refuses promisor lazy fetches, and the
  partial-clone test targets that probe (red without the flag).
- .github/workflows/tests.yml: keep setup-pm; main's uv pin bump does not apply.
  Main's WAL-capable SQLite gates are kept, run against $HERMES_PYTHON (the
  PM-pinned interpreter, SQLite 3.53.1). The e2e step takes main's
  --include-integration invocation.
- apps/desktop: package.json has no build block here, so main's macOS locale-marker
  restore joins the darwin branch of the existing after-pack.mjs, and its test
  loads the hook from electron-builder.config.cjs and imports PlatformPackager
  from app-builder-lib's root (electron-builder 27 exports no ./out paths). The
  win32 row is dropped: this hook sanitizes and signs PE trees on win32 by design.
- reconciliation.ts: main's rowId hydration (#119326) was merged into the first of
  pm-clean's split helpers only; the resolver is now one helper both halves use.
- en.ts: both sides' keys kept. tests/tools/test_lazy_deps.py stays deleted.
- Tests main added with `import yaml` use hermes_yaml, like the rest of the tree.
2026-09-23 19:51:34 -04:00
teknium1
d9e10e0eb1 fix(tui): a secondary profile's key save and session model stay in that profile
One Desktop backend serves several profiles. Two session-bound paths read or
wrote the LAUNCH profile instead of the session's:

- model.save_key was not @_profile_scoped: a key saved from a secondary
  session (session_id) or for an explicit profile landed in the launch
  profile's .env, and the handler then exported it into the shared
  os.environ. It now binds the profile scope like model.options, and the
  explicit os.environ publish is gone (save_env_value already publishes to
  the bound scope, and to os.environ only for the launch profile).
  reconcile_record() already skips a non-launch home, so the profile-param
  guard around it is dropped. model.disconnect had the same gap (it removed
  the launch profile's credentials) and gets the same decorator.
- session.create's info.model and the first state.db row resolved the
  default model from the launch profile's config. _session_default_model()
  resolves it under the session's own profile scope; the same launch-model
  fallback in the lazy resume info, the fallback session info, the live
  session identity and the branch row now use it too.

Found by the two-tenant Desktop backend canary
(tests/e2e/core/tenancy/test_two_tenant_desktop_backend.py): alpha's saved
key appeared in default's .env, and alpha's session.create reported
default's model.
2026-09-23 15:21:15 -07:00
ethernet
9de3d9fdaa Merge remote-tracking branch 'origin/main' into ethie/pm-clean
# Conflicts:
#	tui_gateway/methods_session.py
2026-09-23 08:25:51 -04:00
teknium1
33a30fdd81 fix(tui_gateway): a warm session.resume reports the chat's own model, not the profile default
Reloading a Desktop session that was still live on the backend took the reattach path,
whose `info` said `model: _resolve_model()` (the config default) and carried no provider.
Desktop writes info.model/provider straight into the session state the picker renders, so a
chat pinned to a plugin provider in the composer flipped to the profile default on reload,
and flipped back once the backend had dropped the session and the eager resume restored the
stored override. Reported as "the model changes randomly when reloading" with the Claude
subscription plugin.

`_live_session_identity(session)` answers with the precedence `_session_info` already uses
for the eager path (queued switch, metadata mirror, built agent, the deferred record's
composer override) and falls back to the profile default only when the chat never made a
pick. The reattach `info` now carries `provider` too, like every other resume shape.
2026-09-23 05:00:30 -07:00
ethernet
9f2ba1b74d merge origin/main (779 commits) into ethie/pm-clean
Branch semantics kept where main and PM disagree: update_cmd_deps.py,
constraints-termux.txt, the Electron update-api-check module and the
post-swap hand-off test stay deleted; the pending-fleet-restart catch-up
and the local_runtime tag/download ladder stay retired (PM owns engines).

Ported from main onto the branch's shape: profile_scoped_chore for the
auto-archive and plugin-update housekeeping chores, the local-runtime
cross-process boot lock and residency cap, the checkpoint tmp_pack sweep,
the cua daemon-liveness status probe, the remote-served Desktop update
flag (posix.sh / windows.ps1), sign-in for env-pinned remote gateways
(urlDisabled on RemoteSetupFields), the uvloop extra split (uvicorn
without [standard]), and the umask-scoping spawn test.

uv.lock regenerated with pm.build_env --lock-only; new utf-8 reads from
main switched to utf-8-sig (check-windows-footguns).
2026-09-21 00:58:39 -04:00
fangliquan
a22c29dd5c fix(tui_gateway): scope manual compression to session profile 2026-09-20 11:19:50 -07:00
teknium1
78b5dbfb08 fix(sessions): pin the Bot Chat composer-pick reach through _apply_model_switch -> SessionDB -> session.resume
Replace the direct `_stored_session_runtime_overrides` call test with one that drives the
production path: `_apply_model_switch` on a follow_profile_config row under profile B writes
the composer_override_profile marker into B's real state.db row (`_persist_live_session_runtime`),
and `session.resume` on the deferred path restores the pin while B's config.yaml model is
unchanged and drops it once the profile model moves. Launch home A carries a different model
throughout, so the compare is proven to run under the session profile's scope (A->B->A).
The test is red when the persist hunk in server.py or the record/_resume_eager wiring in
methods_session.py is reverted; the first-write projection in session_workdir.py is pinned by
the extended `_ensure_session_db_row` marker test (a pick before the first send).

One `_row_follows_profile(row)` helper replaces the three copies of
`follow_profile_config or title == "Bot Chat"`; identity is the persisted marker, the raw
title compare stays only inside the helper as the legacy-row fallback.

Docs: one sentence in the Bot Mode guide on the composer pick sticking to the chat until
the Bot's profile model changes.
2026-09-20 10:03:07 -07:00
fangliquanflq
a16d45828f fix(sessions): preserve explicit Bot Chat model overrides 2026-09-20 10:03:07 -07:00
ethernet
9f837d298b Merge remote-tracking branch 'origin/main' into ethie/pm-clean
Conflicts resolved toward the PM model: main's lazy_deps/update_cmd_deps/npm
stamp machinery stays deleted (PM + scripts/build/node-deps.mjs own it), the
systemd ExecStop stop-mark rides the installation launcher, legacy
linux_only/macos_only/windows_only markers are rewritten to platforms(), and
finalize_update_receipt carries pending manual-serve obligations forward
again (lost when the ContextVar receipt rewrite crossed c0aa3ce354).

Test harness: the real-home I/O guard exempts /proc/<pid>/fd metadata reads
(deleted-WAL holder scans) and run_tests.sh drops ~/.hermes PATH entries so
shutil.which() cannot trip the tripwire.
2026-09-20 10:07:50 -04:00
Yagna
e702554e15 fix(tui-gateway): resolve deferred/cold resume overrides under the session profile scope (#115607)
_resume_deferred (Desktop defer_history) and _resume_cold called
_stored_session_runtime_overrides outside _profile_build_scope(ctx.profile_home), so a
secondary profile's stored custom:<name> provider was judged against the LAUNCH profile's
config, "healed" to the launch entry for the same endpoint, and the build inside the
secondary profile died with "Unknown provider". Both paths now bind the session profile
scope like _resume_eager already did.
2026-09-19 22:56:55 -07:00
ethernet
ce3d042f0c Merge remote-tracking branch 'origin/main' into ethie/pm-clean 2026-09-19 23:08:38 -04:00
ethernet
e1576d06a6 Merge remote-tracking branch 'origin/main' into ethie/pm-clean
Resolved toward the branch: PM provisions uv/python (main's install.ps1 uv-shim
salvage + its test and workflow steps dropped), the shim re-exec stays retired,
package.json carries no electron-builder block (afterExtract identity stamp wired
into electron-builder.config.cjs instead; after-pack.mjs keeps signing only),
Desktop workspace-deps helpers stay retired. Main's scratch-dir bootstrap
(export_scratch_tmp_env) is taken and re-run after profile resolution.
2026-09-19 22:57:07 -04:00
Victor Nogueira
1693c071a7 fix: session.create refuses a model its provider cannot serve
A composer, script or older client can pin a model override the selected
provider does not serve (gpt-5.5 on anthropic; the incident pair
deepseek/deepseek-v4-flash-0731 on openai-codex). The gateway minted the
session anyway and the FIRST turn died with the provider's 404, leaving a
dead chat the user had to diagnose and recreate.

session.create now checks the pair before any session state exists and
answers JSON-RPC -32602 naming the model, the provider and up to five
closest models from that provider's curated catalog (error.data carries
them structured). The check is offline and refuses only what Hermes knows
belongs elsewhere: a foreign-family name another native vendor's catalog
lists, or any foreign-family name on the strict OAuth catalogs
(openai-codex / xai-oauth). Custom endpoints, aggregators, same-family
names the curated list lacks and names no catalog lists stay permissive.
Without an explicit provider the pair is judged against the provider the
session would build with (profile config, then env) under the profile's
scope.

Direction and reject-before-side-effects shape from #96845 by
@victorftrdba, trimmed to the offline catalog rule.

Fixes #96817
2026-09-19 12:12:05 -07:00
teknium1
604d8803a1 fix(tui-gateway): session.usage RPC ships provider account limits (account_lines)
The CLI/TUI slash worker and gateway /usage now render Codex quota windows, but the
Desktop usage feed reads the `session.usage` RPC, which returned only Nous
`credits_lines`. Add `account_lines` (the same `render_account_usage_lines` block,
fetched against the session's live route or the configured `model.provider` when no
agent is built) and render it in the Desktop `renderRpcResult` ahead of credits.
Fail-open like the credits block.
2026-09-19 09:40:23 -07:00
ethernet
a6ae6ace51 Merge remote-tracking branch 'origin/main' into ethie/pm-clean
# Conflicts:
#	.github/workflows/js-tests.yml
#	agent/model_metadata.py
#	apps/desktop/electron/main.ts
#	apps/desktop/scripts/bundle-electron-main.mjs
#	apps/desktop/src/app/settings/about-settings.tsx
#	apps/desktop/src/app/settings/gateway-settings.test.tsx
#	apps/desktop/src/app/settings/gateway-settings.tsx
#	apps/desktop/src/app/updates-overlay.tsx
#	gateway/shutdown_flush.py
#	hermes_bootstrap.py
#	hermes_cli/local_runtime/binaries.py
#	hermes_cli/main.py
#	hermes_cli/managed_uv.py
#	hermes_cli/update_cmd.py
#	hermes_cli/update_cmd_deps.py
#	hermes_cli/update_cmd_fleet.py
#	hermes_cli/update_cmd_maint.py
#	hermes_cli/update_receipt.py
#	hermes_cli/update_serve_obligations.py
#	hermes_constants.py
#	tests/hermes_cli/test_doctor.py
#	tests/hermes_cli/test_managed_uv.py
#	tests/hermes_cli/test_pending_supervisor_recovery.py
#	tests/hermes_cli/test_startup_fast_guards.py
#	tests/hermes_cli/test_update_desktop_stale_warning.py
#	tests/hermes_cli/test_update_fleet_restart_pending.py
#	tests/hermes_state/test_hermes_state.py
#	tests/tools/test_tirith_security.py
#	tools/bot_relay.py
#	tools/checkpoint_manager.py
#	tools/write_approval.py
#	website/docs/getting-started/updating.md
#	website/docs/reference/environment-variables.md
2026-09-18 17:26:10 -04:00
finn763
cf398aba40 fix(desktop): record the logged-in user_id on session rows
Desktop sessions landed in state.db with an empty user_id even after a
password login: the backend resolved the identity at WS-upgrade auth (it
writes login_success with the right user_id to logs/dashboard-auth.log) and
stamped it on the session record as auth_user_id, but the row-creating write
never passed it on — and user_id is only ever set at insert, so no later,
identity-aware writer could fill it.

_ensure_session_db_row and _persist_branch (branch children) now stamp the
same <provider>:<id> identity the agent is built with. Anonymous records
carry no login, so those rows keep their empty user_id exactly as before.
2026-09-18 10:16:46 -07:00
teknium1
8c5b66b6a7 fix(tui-gateway): session.set_hidden no longer logs a stored-id hide as a rejected RPC
session.set_hidden is two-tier by design: live runtime id first, then a stored id/key
resolved in the profile db (the Bot Mode sweep and any plugin reconciling sessions it
owns hold stored ids for chats that are not live). The first tier went through
_sess_nowait, which logs "session-scoped RPC rejected: … not in memory (detached/reaped
runtime; client should resume the stored session)" — a warning meant to make a vanished
prompt.submit diagnosable — for every hide that was then fulfilled from the db. A
startup sweep over a handful of stored ids thus wrote a burst of false "rejected" lines
and buried the real stale-runtime-id signal (#114694).

Look the live session up quietly; an id neither live nor stored still returns 4001.
2026-09-18 09:51:32 -07:00
beardthelion
9ac4e76e0f fix(tools,tui_gateway,cli,plugins): parseable non-dict JSON no longer crashes the remaining file scans
Same class as the Bot Chat drain wedge already on this branch: every JSON-file
scan guarded "did it parse?" and then assumed the value was a dict. A file
holding `42`, `"oops"` or `[1,2,3]` (corruption, truncated write, foreign tool)
passed the guard and raised AttributeError/TypeError at the first `.get()`,
usually before a single healthy sibling was processed. Each site now treats a
non-object payload like a corrupt file under that subsystem's existing policy:

- tools/bot_relay.py::_expire_if_stale / claim_pending_envelopes — the
  envelope is skipped by the sweep and not claimed (same as unparseable).
- tools/browser_lightpanda.py::reap_orphaned_lightpanda — record unlinked,
  scan continues.
- tools/write_approval.py::list_pending / get_pending — record skipped with
  the existing "unreadable pending record" warning / None.
- tui_gateway/methods_session.py::_legacy_spawn_tree_entry / spawn_tree.load —
  scalar snapshot reads as empty / returns the existing 5000 error instead of
  violating the SpawnTreeLoadResult contract.
- hermes_cli/local_runtime/binaries.py::manifest_verified — False.
- plugins/platforms/a2a/protocol.py::load_conversation — non-dict lines are
  dropped, keeping the declared list[dict] return.
- batch_runner.py::_load_dataset / _scan_completed_prompts_by_content /
  _combine_batch_files — line skipped and counted as filtered.
- trajectory_compressor.py::process_entry_async — scalar entry passed through
  unchanged.

Ported from the source hunks of PR #114241; its gateway/shutdown_flush.py
drain_transcript_spool hunk is left to open PR #84785, and its
recover_pending_to_db / cron / bot_live_delivery / bot_mode_dm hunks are
already on this branch or on main.

(cherry picked from commit d4b54568887e69b3ee3d363ebe4dcd657ccf64f9)
2026-09-18 09:19:04 -07:00
ethernet
b4a294fff9 Merge origin/main; keep PM as plugin dependency owner
Reconcile plugin declarations and validation through PM's atomic generation publication; preserve external runtimes, target markers, and conflict refusal. Keep one source-update completion owner and port upstream lifecycle changes to the PM desktop/runtime paths.
2026-09-17 13:52:05 -04:00
teknium1
ae1b5d79b2 fix(sessions): one-shot runs get a distinct oneshot source that pickers hide
`hermes chat -q`/`--oneshot`/`-Q` and `hermes -z` (both set HERMES_SINGLE_QUERY_SESSION=1)
persisted their session as `cli` — and, before the first pass, as the inherited
`tui`/`desktop` transport label — so finite automation runs sat in the TUI, Desktop and
dashboard session pickers next to real conversations (#112550).

- run_agent._session_source_for_agent: a single-query run whose source is empty (or an
  inherited UI transport label without an explicit --source) resolves to `oneshot`; the
  platform gate keeps delegate children (`subagent`) untouched; an explicit `--source`
  (HERMES_SESSION_SOURCE_EXPLICIT=1 from main.py) still wins.
- hermes_state_sessions.INTERNAL_LISTING_SOURCES = (kanban, tool, oneshot) replaces the
  three copied `["kanban", "tool"]` literals (tui_gateway session.list, console
  `sessions list`/`stats`, in-chat /sessions), and the Desktop project tree / sidebar
  recents and the dashboard automation set exclude `oneshot` too.
- `hermes -c` / `--resume latest` still chain on the previous one-shot (PR #105957's
  documented flow): the CLI MRU lookup matches the cli family {cli, oneshot} and
  search_sessions accepts several sources; one-shots keep stamping their launch cwd so the
  workspace-scoped lookup keeps working.
- Compression child: the rotated child is published with the PARENT ROW's persisted source
  instead of bare agent.platform, so a `--source tool` / `oneshot` / inherited `kanban`
  session does not degrade to a picker-visible `cli` row after compaction.
- Docs: sessions source table (+ oneshot/kanban/tool rows, compression note) and the
  `--source` flag reference (explicit flag always stored as given).
2026-09-17 09:06:59 -07:00
teknium1
80ab36e4a9 fix(serve): off-turn system-prompt rebuilds run under the session's profile scope
Once a `serve` backend hosts a second profile home, get_secret() fails closed for any
body with no secret scope installed. Two off-turn paths rebuilt the system prompt with
only HERMES_HOME bound (or nothing at all), so the external memory provider's
system_prompt_block() -> get_secret("OPENVIKING_API_KEY") raised UnscopedSecretError
and the launch profile lost its memory block:

- `session.context_breakdown` (tui_gateway/methods_session.py): the Desktop status bar
  refetches it after every turn, which is the once-per-turn warning in #112927; it also
  resolved a secondary session's provider credential and home from the launch profile.
- `_persist_live_session_system_prompt` (tui_gateway/server.py): model switch / one-turn
  restore re-persist. Bound HERMES_HOME alone (#50233); now the full runtime scope.

Both now enter `_session_profile_runtime_scope(session)` (home + secrets + terminal
policy, the launch profile's frozen scope when profile_home is None), the same binding
the turn itself uses. config.show is scoped in the salvaged commits before this one.

Regression (red on base): tests/tui_gateway/test_multi_profile_hosting_fail_closed.py::
test_off_turn_prompt_rebuilds_run_under_the_sessions_profile_scope — A->B->A over two
homes, each rebuild sees its own home and MEM_PROVIDER_KEY, os.environ untouched.

Refs #112927
2026-09-16 10:52:20 -07:00
ethernet
35ade5719d fix(tui_gateway): report desktop_contract in all lazy session-info shapes
The desktop's contract-skew guard reads a missing desktop_contract the same as an old one ((contract ?? 0) < REQUIRED), so any session-info shape that omits the field pops a false "Backend out of date" toast. #36112 fixed the lazy session.create path; three builders still omitted it: _cwd_info (cwd change), _resume_live_unpersisted (resume of a live lazy session with no state.db row), and the workspace-move session.info in agent_callbacks. All client runtime-info paths now carry the field.
2026-09-16 10:34:04 -04:00
outpoints
5237cab756 fix(honcho): thread logical cwd through agent construction
(cherry picked from commit b1d7207c45311be658592c6ad34ee84634fed0ee)
2026-09-15 22:30:11 -07:00
brooklyn!
43bd5db429 fix(state): bound legacy transcript hydration memory
Co-authored-by: Benjamin Brumbaugh <benbrumbaugh@gmail.com>
2026-09-15 12:33:45 -05:00
teknium1
23036e20a6 fix(ux): plain-language, actionable user-facing messages (core)
Squashed integration of the user-facing message audit for this surface set.
Full per-finding receipts: /tmp/ux-audit/lanes/*-receipt.md (campaign artifacts).
2026-09-15 04:12:13 -07:00
teknium1
fdd5995ecb fix(tui_gateway): serve fails closed when hosting a second profile home; profile RPCs bind the full runtime scope
`hermes serve` / the Desktop backend hosted many profile homes (session
profile_home, the `profile` RPC param, hosted rooms) but never called
agent.secret_scope.set_multiplex_active, so every unscoped get_secret read for
a secondary silently returned the LAUNCH profile's os.environ value, and
@_profile_scoped bound only HERMES_HOME: `config.get full` for profile B
expanded B's `${VAR}` refs to the default profile's plaintext credentials,
model.options listed the default's env-keyed providers, llm.oneshot billed the
default's auxiliary key.

- tui_gateway/launch_profile_policy.py (was launch_terminal_policy.py): the
  first time _profile_home registers a non-launch home the process freezes
  the launch env and flips get_secret to fail closed
  (activate_multi_profile_hosting); launch_secret_scope composes the launch
  profile's .env + external sources over that frozen env so systemd / op-run
  injection survives the flip while a secondary never sees it.
- model_switch._profile_runtime_scope_tokens is the ONE composer for
  home + secret + terminal scope: a named profile binds its own files; the
  launch profile binds its frozen-env scope once multiplexing is active and
  stays unscoped in a single-profile process (legacy os.environ precedence).
  _profile_scoped, _profile_scoped_rpc, _session_profile_runtime_scope,
  _bind_build_profile_scopes and _prepare_turn_input all go through it.
- Hosted-room / Group Chat turns for a DEFAULT-profile member in a
  `multiplex_profiles: true` gateway no longer die at agent build with
  UnscopedSecretError: `profile_home is None` was treated as "no scope"
  in _start_agent_build._build and _prepare_turn_input.
- llm.oneshot runs under the session's (or params.profile's) scope;
  _lap_builtin_rows / _overlay_has_creds / _provider_has_credentials read
  provider keys through _scoped_key_env instead of raw os.environ;
  methods_groups._profile_execution_policy resolves the hosted-room policy
  (which reads provider credentials) under the profile's full scope.

Live repro (real `hermes serve`, two homes, config.get {key: full, profile: b}):
base  a_ref: <A_VALUE>  b_ref: ${B_ONLY_TOKEN}  env_ref: <ENV_INJECTED>
head  a_ref: ${A_ONLY_TOKEN}  b_ref: <B_VALUE>  env_ref: ${ENV_INJECTED_TOKEN}
Control (one home, --single): launch config still resolves env_ref from os.environ.
2026-09-15 03:46:29 -07:00