45240 Commits

Author SHA1 Message Date
kshitijk4poor
736a06beac fix(backup): never publish an incomplete full-zip over the previous backup
_write_full_zip_backup_locked published the partial archive to out_path via
_atomic_output_path and only then renamed it to the .incomplete.zip salvage
name, so an incomplete run destroyed a pre-existing good backup at out_path
(test_zip_captures_live_wal_and_cleans_failed_staging[True] regressed vs base).

_atomic_output_path now takes an optional publish_path callable evaluated at
publish time: the full-zip writer publishes the hidden partial straight to
out_path when clean, to the salvage path when some entries failed, and
discards it (returns None) when every entry failed, since an empty salvage
archive restores nothing. out_path is never touched on an incomplete run.
2026-09-27 18:19:17 +05:30
kshitijk4poor
6125c0c707 fix(backup): keep incomplete salvage zips out of retention and the file
Incomplete automatic backups kept the normal <prefix><ts>.zip name, so they
counted toward retention: the next complete run pruned by count and deleted
the last complete backups, and repeated failing runs piled up. Rename them to
<stem>.incomplete.zip, exclude that suffix from _prune_prefixed_zips, and cap
salvage archives at one.

The failed member's bytes also stayed in the file behind a valid local header,
visible to streaming readers as a ghost entry (#124564). Truncate at the first
dropped header and rewind start_dir so later members overwrite it.

Also: name skipped paths in one merged warning, fix a stale comment, drop a
redundant str(), update None-return docstrings, remove an empty duplicate
section heading, and warn in claw migrate when no pre-migration backup was made.
2026-09-27 18:19:17 +05:30
kshitijk4poor
1641b7bbfe test(backup): trim failed-zip-member regressions to two invariants
Keep the automatic-backup partial-member test and the pre-update rotation
test; drop the duplicate-name central-directory test, which exercises the
same _discard_failed_zip_members boundary and pushes the stack past the
two-invariant-test budget for this salvage.
2026-09-27 18:19:17 +05:30
JoaoMarcos44
3a692eb45e test(backup): capture duplicate zip warning
(cherry picked from commit 82fe378fcb26df9a1ee2ae4d9190b912db6e273f)
2026-09-27 18:19:17 +05:30
JoaoMarcos44
eb5c5c549c style(backup): keep test class spacing valid
(cherry picked from commit 6c6d11411299cafb5c18b020416388b9a52ddfd5)
2026-09-27 18:19:17 +05:30
JoaoMarcos44
a71ff10071 test(backup): cover failed zip member cleanup
(cherry picked from commit 857b969c57cd1c7f50b4d5c3d41800e2bc24e4d0)
2026-09-27 18:19:17 +05:30
JoaoMarcos44
da1bc7d502 fix(backup): drop failed partial zip members
(cherry picked from commit e19cb0d5880008098066c987e8edc6ad67b0ab66)
2026-09-27 18:19:17 +05:30
kshitijk4poor
cd5dcd1458 test(compression): parametrize the transport-error case over exception instances
Why: the test parametrized a label string that a ternary in the body mapped
back to an exception, and its name still said "api_timeout" although it also
covers an APIConnectionError carrying the stall marker. Parametrize the two
exception instances directly (with ids) and rename the test to
test_transport_errors_stay_terminal_network_failure. No behaviour change; still
one parametrized test.
2026-09-27 18:19:07 +05:30
kshitijk4poor
f05d11c74b test(compression): pin the TimeoutError guard on the Codex stall reclassification
The isinstance(e, TimeoutError) guard was untested: an APIConnectionError
whose text contains the stall marker must stay a terminal network failure
(#29559/#94448). Parametrize the existing api-timeout test with that input
(red when the guard is removed), and move both #124077 tests into
TestStreamingClosedFailure reusing _fail_on_main instead of a duplicate helper.
2026-09-27 18:19:07 +05:30
kshitijk4poor
d8be403097 fix(compression): match the Codex stall marker on the raw error text
The stall check relied on the lowercased error string, which only works
while CODEX_STREAM_STALL_MARKER happens to be all-lowercase. Match against
str(e) so the shared marker stays authoritative regardless of case, and
fold the two duplicate #124077 comments into one explaining the split
(stall -> retry-ladder timeout; transport timeouts stay terminal).
2026-09-27 18:19:07 +05:30
kshitijk4poor
8c9f2162c7 refactor(compressor): key the Codex stall on the stream guard's shared marker
Co-authored-by: happy5318 <5318happy@users.noreply.github.com>
(cherry picked from commit 83c0a8bb9c7d0e611bafe436d2e4da77144c26ec)
2026-09-27 18:19:07 +05:30
kshitijk4poor
bf8099dab5 test(compression): trim #124077 tests to two invariants, drop local-env warm-ups
The zoneinfo/pydantic plugin warm-up imports were author-machine workarounds
that ran at collection time in CI; remove them. Collapse the class to one
helper and two tests: the Codex stall takes the timeout ladder without the
terminal network-failure flag, and APITimeoutError still sets it.

Co-authored-by: happy5318 <5318happy@users.noreply.github.com>
(cherry picked from commit 1cda73e0ac2d073066741a964b5166b16e9caf34)
2026-09-27 18:19:07 +05:30
kshitijk4poor
3a1a45dbf1 fix(compression): keep transport timeouts terminal; reclassify only the Codex stall
The cherry-picked fix set streaming_closed=False for every timeout, which
also stripped the terminal abort-and-preserve-session behaviour from real
network timeouts (openai APITimeoutError, httpx Read/ConnectTimeout), the
deliberate #29559/#25585/#94448 design. Narrow it: only a TimeoutError whose
message says "stalled" (the Codex aux stream guard) becomes a timeout that
takes the 60/300/900s ladder and does not arm _last_summary_network_failure.
Other timeouts classify exactly as on main.

Co-authored-by: happy5318 <5318happy@users.noreply.github.com>
(cherry picked from commit e013c38a017b4709d4598a0a07c71ea26519312c)
2026-09-27 18:19:07 +05:30
happy5318
56ee7d6f09 fix(compression): classify a Codex stream-guard stall as a timeout, not a terminal network failure (#124077)
## Thinking Path
When the Codex auxiliary stream guard aborts a compaction summary mid-stream
it raises `TimeoutError("Codex auxiliary Responses stream stalled: no new
output for 60.0s ...")`. The message contains neither "timeout" nor
"timed out", so `_classify_summary_failure` returned `timeout=False` while
`_is_connection_error` (which matches the type name "Timeout") returned
`streaming_closed=True`. The terminal network-failure flag then armed an
unconditional abort (`_TERMINAL_SUMMARY_FAILURES`), bypassing the retry
ladder and the deterministic fallback summary — on turn-start preflight
compression that ends in "Auto-resetting session after compression
exhaustion", wiping the session.

### What Changed
`agent/context_compressor.py` `_classify_summary_failure`:
- `timeout` is now computed first, and additionally matches `isinstance(e,
  TimeoutError)` and the "stalled" message shape (the actual text the Codex
  guard emits).
- `streaming_closed` is `_is_connection_error(e) and not timeout` — a
  timeout keeps its retry-ladder semantics and can never arm the terminal
  network-failure abort.

### Tests
New `TestSummaryFailureClassification124077` in
`tests/agent/test_context_compressor.py`:
- classify: Codex stall → `timeout=True, streaming_closed=False`.
- classify: plain `ConnectionError` stays `streaming_closed=True` (no
  regression on the premature-close class).
- classify: a "timed out" message on a non-TimeoutError type stays a
  timeout and is excluded from `streaming_closed`.
- integration: the stalled-summary path in `_generate_summary` does NOT arm
  `_last_summary_network_failure`.

### Verification
- RED/GREEN double proof via git stash: pre-fix 3 failed, post-fix 4/4 pass.
- Regression: 15 existing failure-classification tests pass
  (network_failure / premature_stream / empty_content / auth / truncation).
- ruff clean on changed files.

### Notes
Local test env: this checkout's venv python is a symlink into
`<home>/hermes-agent/.hermes-runtime/...`, so stdlib `zoneinfo` first-import
and pydantic's plugin `distributions()` scan under the real-home IO guard
needed the collection-time warmups at the top of the test file. CI
interpreters are not symlinked into the home — those two warm-up blocks are
no-ops there.

## Related
#124077 (issue). Family: #124078 (the same stall's template trigger),
#108104 (`auxiliary.compression.no_progress_timeout`).

(cherry picked from commit c5399fbdee44f2db1172f22632cbf348a2c7cab5)
(cherry picked from commit bce26a99791c09911d8a008a1922c512f5c1fcea)
2026-09-27 18:19:07 +05:30
kshitijk4poor
008cecddeb chore: map happy5318 for salvage of #124086
Attribute the cherry-picked #124086 commit (author email
5318happy@users.noreply.github.com) to happy5318 in release notes.
2026-09-27 18:19:07 +05:30
kshitijk4poor
0d9329bd95 test(agent): fold async handoff test and dedupe rationale comment
The byte-stability test for the handoff block only re-asserted determinism of
a constant gated on tool-name membership; stable-tier rebuild stability is
already covered by test_system_prompt_restore and test_skills_auto_load. Its
one unique check (block appears exactly once) moves into the positive branch
of the parametrized injection test, and the _prompt helper now takes only the
tool names since every caller used the same model/gates.

The rationale comment lived twice (prompt_builder constant and the
system_prompt call site); keep only the call-site ordering note.
2026-09-27 18:18:12 +05:30
kshitijk4poor
f5028ddd3a test(agent): trim async handoff tests to two invariants
Keep the stack at <=2 invariant tests: the guidance is injected only when
delegate_task is in the toolset (and after the generic keep-working
blocks), and the stable prompt tier stays byte-identical across rebuilds
so the prompt-cache prefix does not drift.
2026-09-27 18:18:12 +05:30
JoaoMarcos44
f4050a00a4 test(agent): pin background delegation yield guidance
(cherry picked from commit 2034413f35c91c462ed20b9e22b74ec5d9a4ef95)
2026-09-27 18:18:12 +05:30
JoaoMarcos44
96a8cecd39 fix(agent): scope async yield guidance to delegation
(cherry picked from commit 99f82de0f99b055bdf5a5ec15be5e662934f2172)
2026-09-27 18:18:12 +05:30
JoaoMarcos44
d9ef15dd3c fix(agent): allow async delegation handoff to end turns
(cherry picked from commit 1378fa1b289ead3b2f8db582353eb4b1574c3d51)
2026-09-27 18:18:12 +05:30
kshitijk4poor
e6f0966b01 fix(terminal): heartbeat error text matches the 0-disables schema
Since 4317ed0e71 the heartbeat schema allows 0 (disabled) and clamps
positive values to 60, but the validation error still said "min 60",
steering models away from the valid 0. Flagged on #119202.
2026-09-27 18:17:27 +05:30
kshitijk4poor
5a85aab191 test(delegate): tidy #121572 follow-ups
elide() already returns short text unchanged, so the eval runner's length
guard was a second copy of the same check. The timeout-diagnostic test
read its log as utf-8-sig although the writer never emits a BOM, and it
interrupted a stub child that never ran.
2026-09-27 18:17:20 +05:30
kshitijk4poor
708bf227fd test(delegate): pin counted marker on elided timeout-diagnostic goal
Keeps #122392's test_timeout_diagnostic_marks_long_goal_as_non_original,
moved into the existing timeout-diagnostic test file and using its
fixture/stub instead of a new file. Red on base (bare marker).

Co-authored-by: Halldrix <12357213+Halldrix@users.noreply.github.com>
2026-09-27 18:17:20 +05:30
Halldrix
0d4dbde837 fix(delegate): mark elided timeout-diagnostic goal and eval tool output
The subagent timeout diagnostic and the session_search eval harness still
appended a bare "...[truncated]" marker, the imitable wording #121548
replaced everywhere else. Route both through agent.compression_marker.elide
so every elision in the tree mints the same counted, guard-matched marker.

Salvaged from #122392 (only the two call-site hunks; base already ships
the elide helpers the PR re-defined). Refs #121572.
2026-09-27 18:17:20 +05:30
Octopustank
a6686cc396 fix(desktop): carry the window app id in the Linux launcher entry
Packaged windows claim `com.nousresearch.hermes` as their Wayland app id
(electron-builder bakes product-identity.cjs's `appId` into
extraMetadata.desktopName; Electron hands that string to the compositor
verbatim), while the entry was written as `hermes.desktop` with
`StartupWMClass=Hermes` — so GNOME matched neither StartupWMClass nor a
`<app_id>.desktop` file name and every launch fell back to the placeholder
icon, with the raw app id in the tooltip.

- write `<app_id>.desktop` with `StartupWMClass=<app_id>` (Name= stays "Hermes")
- retire a leftover `hermes.desktop` once the new entry is on disk, and only
  when the file still names this app and launcher management is enabled;
  foreign files at that path are left alone
- nix/desktop.nix derives the entry file name from the module instead of
  hardcoding it
- tests: the installed entry carries the app id, legacy retirement, foreign-file
  preservation, opt-out preservation, plus a node probe asserting
  APP_ID == product-identity.cjs appId

Known consequence: an existing taskbar pin points at the old entry id and has to
be re-added once.
2026-09-27 07:46:31 -05:00
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
hermes-seaeye[bot]
7d6da01e98 fmt(js): npm run fix on merge (#125233)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-09-27 12:41:42 +00:00
finn763
793517a7d4 fix(desktop): anchor project trunk + at first repo root when primary path unset
Clicking the project-group trunk + passed the null wire path through, which
downstream treats as the reserved Home/detached signal, silently creating a
global session. Fall back to the first repo root for path-less explicit
projects; Home keeps null. Covers click and new-session drag cwd.
2026-09-27 07:30:45 -05:00
chelsealong
f9f153ab18 fix(desktop): clamp Bot Screen clipboard paste to the bridge's 256 KiB cap
clipboardPasteFrom was called with unbounded clipboard text; the gateway's
RfbClientFilter closes the display WebSocket on any ClientCutText over
_MAX_CUT_TEXT (256 KiB), turning an oversized paste into a full stream
disconnect instead of a dropped paste. Clamp before forwarding and add a
test pinning the ceiling.
2026-09-27 07:26:01 -05:00
chelsealong
2d6657d72c fix(desktop): forward client clipboard paste into Bot Screen
Take over over a bot's remote screen never sent the client's text
clipboard to noVNC: screen-pane.tsx built the RFB session but had no
paste handler and never called clipboardPasteFrom, so Cmd/Ctrl+V into
a remote input did nothing (#123089). The gateway already gates
ClientCutText as lease-checked input, so the missing leg was purely
client-side.

Add an explicit-gesture `paste` listener on the canvas, forwarded only
while this viewer's RFB client is not view-only (i.e. holds the
lease); no polling, no clipboard logging. Listener is torn down with
every detach so it never outlives its RFB client.
2026-09-27 07:26:01 -05:00
Brooklyn Nicholson
e9c60209a7 fix(desktop): bridge the already-decoded media stream path once
The hermes-media://stream resolveLocalFile boundary ran
resolveMediaRequestPath on the path the protocol handler had already
percent-decoded and slash-stripped (parseMediaProtocolTarget), so the
second decode+strip turned every absolute POSIX media path
(/home/.../.hermes/...) into a cwd-relative one. The resolver then
pinned it under the app's cwd and the file ENOENTed into the handler's
silent 404 — inline TTS/audio playback dead with no log line, which is
the Linux symptom in hermes-agent 123823. A second decodeURIComponent
also threw URIError on filenames containing a literal %.

Restore the single decode at the protocol layer: the boundary now only
bridges (resolveMediaStreamFile), matching what 7722761581 intended as
byte-for-byte behavior but broke when it rewired main.ts.

Fixes https://github.com/NousResearch/hermes-agent/issues/123823
2026-09-27 07:20:20 -05:00
Brooklyn Nicholson
ba5e3bfa31 fix(desktop): say "File not found" when opening a missing file
Opening a deleted/renamed artifact reported "No application found to open
URL": macOS LaunchServices answers a non-existent path with
kLSApplicationNotFoundErr (-10814), and the open route handed resolved
paths to the OS without ever stat-ing. On current main the file route is
reveal-in-folder only, which is worse on a miss — showItemInFolder of a
non-existent path is a silent no-op on macOS, so the click does nothing
at all.

- hardening.ts: assertExistingPathForOpen — pre-open stat wired into
  openExternalFile before the reveal-dedupe window is touched (a miss
  must not poison dedupe against a legitimate retry). Only
  ENOENT/ENOTDIR become an honest missing-file failure.
- external-open.ts: reportPreOpenStatFailure — the guard's
  classification lives in the electron-free module with the rest of the
  open logic, so it unit-tests without electron. A miss is reported and
  the route stops; every other stat failure (EACCES on a locked volume,
  ELOOP, Windows EPERM, ENAMETOOLONG) is logged and the OS is still
  asked, so an existing-but-locked file keeps its real error — a stat
  failure never fabricates a miss and never swallows the click.
- preload.ts: the failure dialog merged in #122023 had no preload
  forwarder for hermes:external-open-failed, so every open failure was
  silently swallowed in every window (the optional-chained bridge is
  invisible to typecheck). Adds the onExternalOpenFailed listener plus a
  test tripwire so the dead wiring cannot come back.
- The failure payload gains code: 'missing-file'; the dialog shows
  localized "File not found" copy for it instead of the no-browser
  text (i18n types + en).

Fixes https://github.com/NousResearch/hermes-agent/issues/122027

Co-authored-by: Andrew Ho <andrewho.sf@gmail.com>
Co-authored-by: Enough1122 <10966420+Enough1122@users.noreply.github.com>
2026-09-27 07:10:01 -05:00
Brooklyn Nicholson
68a9817b57 fix(desktop): background Bot Chat re-resumes never navigate
A session.reclaimed (or roster-activity) wake re-resolves the open bot
chat so the next send doesn't eat a stale runtime id — but it went through
the full navigating open, so with the Kanban board (or any other route)
in the foreground the route flipped to the Bot Chat. Background events
must offer, not hijack.

host.openSession gains refreshInPlace: the wake still consults the
registry, dials the owner backend, and stamps the owner hint, but never
calls the core open (route/tile), never re-publishes the bots workspace
scope, and never flips the all-profiles view. It refreshes through the
same levers the SDK's own hydration probe uses — the tile delegate's
resumeTile(refreshTranscript), or the armed requestSessionResume that
the route-resume effect consumes only while the route already points at
the session. A chat that isn't on screen re-opens nothing.

openBotCanonicalChat threads this as background: true, and a background
re-resume never MINTS a missing registry row. The reclaim listener and
the roster-activity refresh both pass it.

Fixes https://github.com/NousResearch/hermes-agent/issues/121874
2026-09-27 07:05:20 -05:00
Brooklyn Nicholson
db1728d35d fix(desktop): keep authored link labels and identifier casing
MarkdownLink dropped formatted link labels: childrenToText only handled
plain strings, so an inline-code label ([`v1.0.1`](url)) fell through to a
title fetch or a URL-slug fallback that title-cased the identifier.
Flatten element children so the authored label wins with exact casing,
and keep the casing of separator-less slug tokens that look like
identifiers (digits, dots, mixed case) instead of title-casing them.

Fixes https://github.com/NousResearch/hermes-agent/issues/121321
2026-09-27 07:03:56 -05:00
Xipong
e0bc2c1ce9 fix(desktop): retain access to past tasks after turns 2026-09-27 07:03:12 -05:00
Brooklyn Nicholson
10b24064dd fix(desktop): drop backdrop blur when the OS asks for reduced transparency
Add a blanket prefers-reduced-transparency gate to styles.css that sets
backdrop-filter: none everywhere, mirroring the existing blanket
reduced-motion rule. Frosted surfaces (composerDockCard via
composerSurfaceGlass, menus, popovers) re-sample their backdrop every
animation frame; on transparent plugin wallpaper themes a late filter
pass paints unblurred and the whole docked area strobes (issue 121910).

The alpha-mixed fills (--composer-fill at 88%) carry legibility on
their own, so nothing changes visually unless the OS accessibility
signal is on.
2026-09-27 07:01:33 -05:00
Brooklyn Nicholson
461b7f0caf test(desktop): pin the docked widthOverride across pane churn
Invariant companion to the narrow-overlay width fix: the docked tree
path (routines register/unregister across botChatOwnsWorkspace edges,
workspace removeTreePane + re-adopt, bots pane unregister/re-register
with enforced dock) preserves the sash-dragged widthOverride — the
narrow overlay was the only path that discarded it.
2026-09-27 06:55:18 -05:00
Brooklyn Nicholson
351d59be8f fix(desktop): narrow edge overlay honours the dragged pane width
The narrow edge overlay sized itself from the pane's declared data.width
(260px for bots), discarding the persisted widthOverride the sash drag
wrote, so every chat-switch reveal snapped the panel back to its declared
width. Size from the same fixedTrackSize resolution the pane's docked
zone uses, extracted as a pure narrowOverlayWidth helper so the
resolution itself is testable under jsdom.

Co-authored-by: kokhlo <47825603+kokhlo@users.noreply.github.com>
2026-09-27 06:55:18 -05:00
Brooklyn Nicholson
a1dcbc20ad fix(desktop): stop transcript images overhanging the next block 2026-09-27 06:55:15 -05:00
Brooklyn Nicholson
5b39269e1a fix(lint): sort imports 2026-09-27 06:54:56 -05:00
Brooklyn Nicholson
0866af3b6b chore: map contributor emails 2026-09-27 06:54:56 -05:00
Brooklyn Nicholson
2326900129 fix(desktop): import clearClarifyRequest from the clarify store
Follow-up to the stale-approval fix: clearClarifyRequest lives in
@/store/clarify, not @/store/prompts — the wrong-module import slipped
through because the prompts barrel happens to export similarly named
clears. Caught by the vitest run of the new specs.
2026-09-27 06:54:56 -05:00
mooserini
fa655b2980 fix(gateway): do not auto-TTS A2A replies
voice.auto_tts (flipped globally by Desktop "Read replies aloud") made
the runner synthesize a spoken reply for every A2A text task. The A2A
adapter has no native send_voice, so delivery fell back to the media
notice — the peer received "Couldn't deliver the audio attachment."
instead of the text the agent had already produced (#90103). Inbound A2A
is MessageType.TEXT; the base adapter's own auto-TTS gate keys on
MessageType.VOICE, but the runner's fallback branch
(voice_mode is None and adapter_auto_tts) had no platform gate at all.

Two aligned gates, both field-tested by the reporter:

- _should_send_voice_reply returns False for platform 'a2a' before any
  mode/config consultation — the text reply lands normally.
- _sync_voice_mode_state_to_adapter never pushes the global speak
  default onto the A2A adapter, so the adapter-side path cannot regress
  it either.

/voice on|tts|off scoped behavior is untouched (persisted per
platform:chat_id, still honored for human platforms). Tests pin the
pair the issue asked for: A2A + global auto-TTS skips, Telegram with
the same default still voices, and the sync-side skip.

Re-implemented from PR #90121 on the GatewayVoiceMixin split (author
credited). The Desktop-scoped read-aloud preference is a separate
design change, not folded in.

Fixes #90103
2026-09-27 06:54:56 -05:00
BlackishGreen33
55e27e5dbe fix(desktop): explain missing profile SOUL.md files
The backend already reports {content, exists} from GET
/api/profiles/{name}/soul, but both SOUL.md editors read only content —
a profile whose personality lives in config.yaml
(agent.personalities.<name>) rendered a blank CodeEditor with no hint
that anything was missing or where the persona actually lives (#89436).

Both surfaces (Profiles view editor and the sidebar Edit SOUL.md
dialog) now show a localized notice when exists is false: no SOUL.md
exists yet, instructions below will create one on save, and config.yaml
personality presets are managed separately. The notice clears on a
successful save; an existing empty file or a read error is never
misreported as missing.

i18n: soulMissing added to every full locale (de/es/fr are override
locales and fall back to English). Component tests cover the
missing/empty/read-error distinction and cross-profile leakage; an e2e
spec drives both editors end to end.

The config.yaml read-back fallback and the dashboard source indicator
stay deferred to the personality-precedence work (#82801).

Salvaged from PR #105201 (author preserved).

Fixes #89436
2026-09-27 06:54:56 -05:00
jackulau
9f5440e23d fix(agent): report the conversation category even when it is empty
The category filter dropped every zero-token category from the breakdown
payload. For mcp/memory/skills that is right — zero means "not
configured" and the row's absence says so. For conversation it hid the
row on any session whose transcript was empty or pre-turn, so the
Desktop Context usage panel showed System prompt/Tools/Memory but no
Conversation until the first turn completed (#87903): "the transcript
is empty" rendered identically to "the breakdown never measured it".

Zero for the conversation is a MEASUREMENT of something every session
has, so it is exempted from the drop via _ALWAYS_REPORTED; the
membership rule is documented at the constant so later additions argue
from the same principle. Structurally absent categories stay dropped.

Test: an empty-transcript breakdown reports conversation at 0 while
unconfigured optional categories remain omitted.

The desktop half (retained pre-turn snapshot) is already fixed on main:
useContextBreakdown nulls the snapshot mid-turn and the statusbar gauge
falls back to the streamed usage.

Python half salvaged from PR #87925 (author credited).

Fixes #87903
2026-09-27 06:54:56 -05:00
pasmud
8b7a1620a1 fix(review): surface real gh stderr in PR-create errors
Every non-zero gh exit collapsed to a generic "is gh installed and
authenticated?" — a lie whenever gh was fine and the real failure was
"no commits between main and feature", a missing upstream, or a refused
push (#87731). The user had to drop to a terminal to learn what gh
already printed.

- apps/desktop git-review-ops.ts runGh() now resolves {ok, stdout,
  stderr} (execFile's err.stderr carries the exit's own stderr), and
  reviewCreatePr() prefixes the surfaced message with gh's reason,
  keeping the generic text only when gh reported nothing.
- hermes_cli web_git.py _gh() keeps (ok, stdout, stderr) through the
  same collapse — _run already captured stderr; it was discarded at the
  tuple boundary — and review_create_pr() surfaces a bounded stderr
  tail (400 chars) plus the gh context.

Tests pin the contract with unique stderr markers and non-zero exits on
both wrappers; successful-path return contracts unchanged.

Electron half salvaged from PR #87751 (author preserved); CLI half added
per the issue's acceptance scope.

Fixes #87731
2026-09-27 06:54:56 -05:00
Brooklyn Nicholson
bda04a699a fix(desktop): stop finished turns from showing stale approval prompts
Two clear paths missed, both leaving a per-session approval entry parked
after the turn ended — the floating "↓ needs approval" bar then
reappeared on a session the sidebar already showed as finished, whenever
scrolling unmounted the inline anchor (#86577):

- session.reclaimed now clears the prompts keyed to the reclaimed
  runtime id. The runtime id rotates on every resume, so the NEW
  runtime's turn-end edges can never remove an entry keyed to the old
  one; a reopened conversation remounted the stale bar.
- a running=false session.info for a session we knew was live (busy or
  awaitingResponse) now clears its prompts. The agent loop's finally
  block emits running=false even when a reconnect gap or crash swallowed
  message.complete — the only existing turn-end clear — so the terminal
  edge doubles as an authoritative prompt clear. Bystander sessions are
  untouched: both clears are scoped per session id.

Tests: the reclaimed-runtime approval retires while a bystander keeps
its prompt; a missed-complete turn retires its approval; an idle
session's running=false heartbeat does not.

Re-implemented on the split gateway-event modules from PR #86616
(author credited); the needsInput sidebar-dot half stays with the closed
sibling #86565.

Fixes #86577

Salvaged from a fix by fangliquanflq (GitHub account since removed).
2026-09-27 06:54:56 -05:00
Brooklyn Nicholson
01f468a297 chore(attribution): re-attribute 0f4a98f87c to OutThisLife
Original: 0f4a98f87c
Subject:  fix(desktop): pair repeated-caption attachment folds by paste ordinal (#122079)

The original commit was authored as `Hermes Agent <agent@hermes.local>`,
an unverifiable address, so GitHub credited it to no one. This commit
restores attribution to its author; it changes no code.

Re-attributes: 0f4a98f87c
2026-09-27 06:53:39 -05:00
Brooklyn Nicholson
e1683b2f5d chore(attribution): re-attribute 820bb0b1af to OutThisLife
Original: 820bb0b1af
Subject:  fix(desktop): keep the attachment-tolerance dedupe inside the identity gate (#122079)

The original commit was authored as `Hermes Agent <agent@hermes.local>`,
an unverifiable address, so GitHub credited it to no one. This commit
restores attribution to its author; it changes no code.

Re-attributes: 820bb0b1af
2026-09-27 06:53:39 -05:00
Brooklyn Nicholson
003d34184f chore(attribution): re-attribute 056a960870 to OutThisLife
Original: 056a960870
Subject:  fix(desktop): never tolerance-match a captionless attachment turn (#122079 review)

The original commit was authored as `Hermes Agent <agent@hermes.local>`,
an unverifiable address, so GitHub credited it to no one. This commit
restores attribution to its author; it changes no code.

Re-attributes: 056a960870
2026-09-27 06:53:39 -05:00