Commit Graph

28243 Commits

Author SHA1 Message Date
kshitijk4poor 5d4ad23b0d chore: map paulapsp157@gmail.com -> Aoshi-Dev (PR #96003 salvage) 2026-08-27 14:02:06 +05:30
Aoshi-Dev 5ab04c764c test(hermes_cli): make test_default_path pass on native Windows
TestGetHermesHome.test_default_path asserted ~/.hermes unconditionally,
but the native Windows default is %LOCALAPPDATA%\hermes (see
hermes_constants._get_platform_default_hermes_home). Branch the
assertion by platform so the test passes everywhere.

Salvaged from PR #96003 by @Aoshi-Dev (the parse-guard half of that PR
was superseded by #96169); authorship preserved.
2026-08-27 14:02:06 +05:30
kshitijk4poor 93a29d110d fix(hermes_cli): surface fail-closed config write refusals cleanly
Follow-ups to the salvaged #71385 guard (which raises RuntimeError from
require_readable_config_before_write on unparseable / non-mapping YAML):

- config_command: catch RuntimeError for set/unset and print a clean
  one-line error + exit(1) instead of a raw traceback on the primary
  'hermes config set/unset' CLI path.
- console_engine._capture_output: convert escaping RuntimeError into a
  ConsoleCommandError so 'hermes console' and the dashboard console
  report the refusal instead of crashing the REPL/websocket session.
- _warn_config_parse_failure: add a dedicated 'refuse-write' wording
  branch — the old fallthrough claimed 'falling back to default config'
  even though the write was refused and the file preserved.
- approval_mode: update the stale SystemExit-only comment.
- Regression tests for the console path and both config_command paths.
2026-08-27 13:27:24 +05:30
fangliquanflq df8b841c0c test(hermes_cli): align malformed YAML set/unset expectations with RuntimeError 2026-08-27 13:27:24 +05:30
fangliquanflq 77d4d23fbc fix(hermes_cli): stop config set/unset from wiping user overrides on invalid YAML
Fail closed when config.yaml is unparseable or non-mapping before set/unset writes, reuse the readable-config guard to return the parsed mapping, and cover refuse/empty-mapping paths with regression tests.
2026-08-27 13:27:24 +05:30
kshitijk4poor c693c772b5 refactor(compression): fold review follow-ups on #71488 salvage
- Reuse the existing _commit_status variable for the terminal-edge gate
  instead of the parallel _compaction_succeeded boolean (derived state).
- Give the commit_fence_cancelled abort the same force_terminal=True
  terminal edge as the lock-contended abort, and reword the closure
  comment that overstated the lock contender as 'the one exception'.
- Inline the codex app-server path's lifecycle closure: after gating on
  success it reduced to a single success-site emit, so the scaffolding
  (done-flag + closure + two no-op failure-path calls) was dead.
2026-08-27 12:42:38 +05:30
Uttkarsh Tiwari f5abf68add test(compression): cover failed in-place split status 2026-08-27 12:42:38 +05:30
Uttkarsh Tiwari 36ae32524c fix(compression): preserve terminal lifecycle for lock skips 2026-08-27 12:42:38 +05:30
Uttkarsh Tiwari 7a21bfe68a fix(compression): suppress duplicate completion notices 2026-08-27 12:42:38 +05:30
Ben Barclay ecf327c872 test(telemetry): make the renewal-extension regression falsifiable
Eighth review round (the first against the atomic-renewal fix) verdict:
the production code holds - CAS exclusivity across real processes,
lease-extension schedules, clock skew both directions, renew-per-attempt
under 5xx backoff, defer accounting, and the author's mutants all
verified - but one shipped regression test could not fail against the
property it is named for.

test_renewal_extends_the_lease_across_the_post asserted
next_attempt_at >= lease_before under a frozen clock. A renewal that
matches the row but never extends the lease (M4: SET next_attempt_at =
next_attempt_at) satisfies >= trivially, and that mutant double-POSTs:
the un-extended lease expires mid-POST and a second process reclaims.
The reviewer demonstrated M4 surviving the whole suite while producing
a real duplicate send in a two-process schedule.

The test now renews 100s into the lease from an advanced clock and
requires the deadline to move strictly forward to exactly
renewal-clock + 300s. Verified: M4 now fails this test (61 others
unaffected); clean HEAD passes all 62.

No production code change. 277 tests; ruff + footguns clean.
2026-08-27 16:36:11 +10:00
Gille 6defe7eb6c fix(config): preserve lossy decimal values as strings 2026-08-27 11:49:58 +05:30
kshitij 2ea42a44e0 Merge pull request #96118 from kshitijk4poor/revert/94245
revert: WS-only desktop backend (#94245)
2026-08-27 11:29:25 +05:30
kshitijk4poor 9f05b06589 Revert "Merge pull request #94245 from kshitijk4poor/feat/gw-event-replay"
This reverts commit df7d7f6e8d, reversing
changes made to 1a66134404.
2026-08-27 11:26:57 +05:30
kshitij df7d7f6e8d Merge pull request #94245 from kshitijk4poor/feat/gw-event-replay
feat(gateway): slim WS-only server — remove FastAPI/uvicorn from desktop boot path
2026-08-27 11:22:41 +05:30
Ben Barclay 1a66134404 test(relay): cover media-lane unfurl stamping and descriptor fallback
The send_media lane (08b95c3) had no committed regression test: cover
explicit-bool stamping on media frames, the descriptor-platform
fallback when _platform_by_chat is empty (post-restart proactive
sends), and the omitted-key absence case.
2026-08-27 15:33:32 +10:00
Ben Barclay ff9930037b docs(slack): note caption ordering and streaming fallback for unfurl knobs
When either unfurl key is set, media captions post as a separate
message before the file (the upload API cannot carry unfurl controls)
and native draft streaming falls back to edit-based delivery. Surface
both side effects in the config reference table.
2026-08-27 15:33:32 +10:00
Ben Barclay be5c75165f fix(relay): fall back to descriptor platform for unfurl stamping
The send and send_media lanes resolved the platform only from
_platform_by_chat, which is empty until an inbound frame arrives (e.g.
after a gateway restart). A proactive send to a Slack chat then missed
the unfurl stamp. Mirror the streaming gate and delivery resolver:
fall back to the negotiated descriptor's platform.
2026-08-27 15:33:32 +10:00
Ben Barclay 5521265def fix(slack): coerce string unfurl knobs on the native plane
Relay-plane parity: hermes config set / Railway persist YAML booleans
as strings, and _slack_unfurl_kwargs silently dropped them — so
'unfurl_links: "false"' was a no-op on native while working on relay.
Coerce recognized string booleans exactly as _slack_unfurl_hints does;
unrecognized values still drop so junk config keeps Slack's default
instead of accidentally suppressing previews.

Replaces test_send_ignores_non_boolean_unfurl_options (which froze the
dropped-string behavior) with coercion + junk-drop tests.
2026-08-27 15:33:32 +10:00
Victor Kyriazakos 634d9c3f4c fix(relay): coerce string unfurl knobs and disable Slack draft streaming
Live staging (Coatue Slack):
- hermes config set / Railway knobs persist "true" as a string; bots that
  omit unfurl_links do NOT inherit the human default, so dropping the
  string looked like suppression.
- chat.startStream cannot carry unfurl_*. Native SlackAdapter already
  falls back to chat.postMessage; the relay now matches.
2026-08-27 15:33:32 +10:00
Victor Kyriazakos db00793db7 chore(contributors): map potatosaladx@gmail.com to potatosalad (attribution for salvaged #79436/#81128 commits) 2026-08-27 15:33:32 +10:00
Victor Kyriazakos cc2fb46275 feat(relay): stamp slack unfurl_links/unfurl_media onto outbound frame metadata (gateway-directed)
Relay-fronted Slack reads platforms.relay.extra.slack.unfurl_links/unfurl_media
and stamps explicit booleans onto the frame metadata; the connector forwards
them to chat.postMessage with no config of its own (mirrors reply_in_thread).
Covers send, send_for_platform (cron/scheduled), and send_media lanes.
2026-08-27 15:33:32 +10:00
Will Lynas 91dbd7a6f8 fix(slack): preserve unfurl controls during streaming 2026-08-27 15:33:32 +10:00
Will Lynas b91845c2a0 fix(slack): honor unfurl controls for media captions 2026-08-27 15:33:32 +10:00
Will Lynas f7c87d9536 test(slack): cover default and split unfurl behavior 2026-08-27 15:33:32 +10:00
Andrew Bennett aeaa0c784e feat(slack): add link unfurl controls 2026-08-27 15:33:32 +10:00
Ben Barclay 4bdabb21ed fix(telemetry): renew the claim atomically before every POST
Seventh review found the claim-token fix incomplete, and its
reproduction is exact: the pre-POST check was READ-ONLY. A claimant
whose lease expired while suspended still passes it when it wakes
BEFORE anyone reclaims - its token is still in the row - and then a
second process legitimately reclaims while the first one's POST is in
flight. Both send. Reproduced at 60addb16e2: posts ['B', 'A'], both
reporting 'sent'. This is the check-to-POST expiry race, not the
documented mid-POST residual: A's lease was already dead before its
authority check passed.

The check is now an atomic RENEWAL (single CAS UPDATE): it requires the
token to match, the row to be pending, AND the current lease to be
unexpired, and only then extends next_attempt_at a fresh lease into the
future. rowcount == 1 is the only grant. A claimant that wakes past its
own lease fails the unexpired condition and yields even though its
token was never replaced - expiry alone means another process may
claim at any moment, so waking stale is disqualifying regardless of
whether anyone has taken the row yet. The renewed lease (300s) covers
the POST (30s timeout) with margin, and renewal runs before every
retry, not just the first attempt.

Regressions: the reviewer's exact ordering (expired wake before any
reclaim -> zero POSTs, row stays claimable), plus a healthy-claimant
renewal test. Mutation-checked: dropping the lease-unexpired condition
or the token condition each fails the suite.

The at-least-once scope note on _send_one stands: a suspension landing
mid-POST remains client-unfixable; the fixable window is now closed on
both sides (before the check, and between check and POST).

277 tests pass; ruff + footguns clean; staging E2E 202.
2026-08-27 15:21:30 +10:00
Ben Barclay 29033a3fd5 fix(relay): restore voice-note STT — wire media[] MIMEs, message_type "voice", and a User-Agent for CDN downloads (#95274)
* fix(relay): map wire media[] → event.media_types; accept message_type voice

A relayed voice note arrived as MessageType.AUDIO with media_types=[] —
the STT gate (_event_media_is_stt_input) excludes AUDIO unconditionally
and its per-attachment MIME rescue was unreachable, so STT never fired
and the agent fell back to the "user sent an audio file attachment"
context note (live-verified on staging 2026-08-26, Discord + Telegram).

Two wire-boundary fixes, both additive within contract_version 1:

- "voice" parses to MessageType.VOICE: the enum already had it — pinned
  by test so a future refactor can't collapse the two.
- media[] is now mapped into event.media_types (positional alignment
  with media_urls; mime-less entries keep their slot as ""). This is
  what run.py's per-attachment classifiers key off, so EVERY relayed
  attachment — image vs document, audio vs voice — now routes like its
  native-adapter equivalent, not just voice notes.

Behaviour pinned: new-connector voice → STT-eligible; legacy
audio-typed events unchanged (no STT); music uploads never STT-eligible
(direct _event_media_is_stt_input assertions on real wire-parsed
events, not mocks).

Pairs with the gateway-gateway PR that puts "voice" on the wire.

* review: pin the STT gate by test; fail safe on media/media_urls mismatch

Addresses independent review of #95274.

1. The PR's acceptance criterion is STT ROUTING, but no committed test
   called _event_media_is_stt_input — it was only asserted ad-hoc. Adds
   TestSttGate: voice→eligible, voice-without-media_types→eligible
   (the new-connector/old-gateway shape), legacy audio-typed voice
   note→not eligible, music→not eligible. Mutation-verified: removing
   the VOICE branch from the gate turns these RED.

2. media_urls and media[] are INDEPENDENT wire fields that consumers
   index by the same i. Mapping MIMEs positionally without checking
   agreement means a disagreeing producer misassociates a MIME with the
   wrong URL and mis-routes that attachment — strictly worse than no
   MIME, which degrades safely to message-level classification.
   _media_types_from_wire() now maps only when the lengths agree, warns
   and returns [] otherwise.

Note for the record: MessageType.VOICE predates this PR and the gate's
VOICE branch ignores media_types, so a NEW connector against an OLD
gateway ALREADY fires STT. That is desirable, but it is not "unchanged"
— the PR body's rollout matrix said otherwise and is corrected.

* fix(relay): send a User-Agent on relay media requests (Discord CDN 403)

Discord's CDN rejects urllib's default "Python-urllib/x.y" User-Agent
with HTTP 403, and RelayMediaClient never set one. Every Discord CDN
pass-through download therefore failed; _localize_inbound_media then
kept the raw URL (its "a public URL still has value" branch), and the
consumer tried to open a URL as a FILE PATH:

  WARNING gateway.relay.media: relay media download failed for
    https://cdn.discordapp.com/...voice-message.ogg: HTTP Error 403
  INFO gateway.run: Voice transcription failed for https://cdn.discord...
    : Audio file not found: https://cdn.discordapp.com/...

This killed ALL Discord relay media inbound — voice notes, images and
documents alike — not just the voice lane. Telegram/WhatsApp were
unaffected because their media is connector-re-hosted (/relay/media/{id},
fetched from our own host) and localizes to real /tmp paths.

Reproduced from a clean shell against a live CDN URL:
  curl (own UA)                -> 200
  urllib, no UA                -> 403 Forbidden
  urllib + descriptive UA      -> 200, 14583 bytes, OggS magic

Fix: a module-level _MEDIA_USER_AGENT sent on both download() and
upload(). upload() only ever targets our own connector so it was not
broken, but a single client should identify itself consistently.

Validated on staging: hot-patched hermes-agent-stg-test-6698, restarted
the gateway service, and Ben's Discord voice note transcribed
successfully — zero new 403s and zero new transcription failures after
the patch (last 403 predates it).

Test is mutation-verified: removing the UA from download() turns it RED
while the other five media tests stay green.

* fix(relay): keep url↔mime pairing through media localization

Addresses a blocking review finding on my own change: mapping media[]
into media_types created a POSITIONAL contract that the rest of the
inbound path then broke.

1. _localize_inbound_media (adapter.py) filtered media_urls without
   filtering media_types. Dropping a dead connector re-host is a NORMAL
   best-effort path, so every surviving attachment inherited its
   neighbour's mime. Reproduced through the real functions:

     before urls  [.../relay/media/dead, .../kept.png]
            types [application/pdf, image/png]
     after  urls  [.../kept.png]
            types [application/pdf, image/png]   <-- PNG reads as PDF
     _event_media_is_image(ev, 0) -> False

   The loop now carries (url, mime) as PAIRS, so a dropped URL drops its
   mime with it.

2. _media_types_from_wire compared LENGTHS only, which is not alignment:
   equal-length-but-reordered wire fields were accepted and paired
   wrongly, and an absent media_urls skipped the check entirely while
   still emitting types. Resolution is now BY URL (url -> mime lookup
   over media_urls); an unmatched URL degrades to "" and falls back to
   message-level classification.

Tests: 4 new cases driving the real chain (wire parse -> localization ->
run.py classifier), incl. the dropped-first-attachment case the existing
localization test could not catch (it builds events without
media_types). The obsolete length-mismatch test now asserts the stronger
by-url guarantee. Both fixes mutation-verified: reinstating the URL-only
filter fails 1 test, reverting to positional resolution fails 3.

Relay suite 258 passed; media/voice/stt selection 685 passed; ruff clean;
cross-repo integration payload re-verified.

* fix(relay): media_types is always one slot per media_url

Self-review after two review rounds flagged this bug class in adjacent
seams: I checked the function I edited, not every consumer of the
parallel arrays I created. Grepping ALL writers found a third instance.

merge_pending_message_event (gateway/platforms/base.py:2725-2735)
EXTENDS media_urls and media_types together when a second media message
merges into a pending one. My mapping could emit a POPULATED media_urls
with an EMPTY media_types (an older connector sends media_urls but no
media[]), so extend() concatenated lists of different lengths:

  A urls [old1.png, old2.png]  types []
  B urls [new.pdf]             types [application/pdf]
  merged urls  [old1.png, old2.png, new.pdf]
         types [application/pdf]
    -> old1.png reads as application/pdf; the real PDF gets ''

Fix: media_types is now ALWAYS len(media_urls), padded with '' — the
url-keyed lookup runs even when media[] is absent, and the localizer
rewrites the list unconditionally (no  short-circuit that
could leave a stale/short list behind).

Tests: 4 new cases — padding with no media[], the merge shift above
driven through the real merge_pending_message_event, localization
preserving the invariant while dropping an entry, and normalization of
a short/empty media_types arriving from a non-wire source. All
mutation-verified: removing the padding fails 4; restoring the
 guard fails 1.

Relay 262 passed; media/voice/stt selection 689 passed; ruff clean;
cross-repo integration payload re-verified.
2026-08-27 15:07:38 +10:00
Ben Barclay 60addb16e2 fix(telemetry): fence send authority on a per-claim token
Responds to the independent PR review (andrexibiza). Both P1s were
checked against current HEAD rather than taken on authority - the
review was written against 613849c190, before the interval-model
consent replacement landed.

P1-1 (same-UTC-day revoke/re-enable releases refused data): already
fixed by the interval model. The reviewer's exact reproduction - opt in
06:00, revoke 12:00, package collected 18:00, re-enable 20:00 same day
- was re-run at HEAD: the off-window package stays local, and a full-day
aggregate straddling the revocation boundary also stays local (period
containment, timestamp precision). The consent-windows harness already
pins both. The reviewer's related ask that consent-ledger persistence
failures fail closed also holds structurally now: reconciliation derives
state rather than recording transitions, so a lost write means a shorter
confirmed horizon - less is released, never more.

P1-2 (lease has no owner) was VALID at head. Reproduced exactly as
described: A claims, is suspended past the 300s lease, B reclaims and
POSTs, A resumes and POSTs again - and the ingest key is minute-
prefixed, so the duplicate lands as a DISTINCT stored object, making
this worse than a benign idempotent overwrite.

Fix: every claim now mints a claim_token (additive nullable column,
schema version unchanged). Ownership is revalidated immediately before
every external POST, and every settlement, rejection, and backoff write
is compare-and-set on (package_id, claim_token, pending). A lapsed
claimant that resumes yields without transmitting, and its stale
backoff cannot move next_attempt_at under the live claim's lease.

Two deterministic regressions ship with it: expiry -> reclaim -> resume
(the reviewer's schedule), and the subtler stale-backoff-clobber case.

Honest scope, documented on _send_one: delivery remains at-least-once.
The token closes the claim->POST gap; a suspension landing mid-POST
(bytes already on the wire) is not client-revocable. The residual
duplicate is byte-identical content; collapsing it fully needs
package_id-keyed dedupe at the ingest service.

275 tests pass; ruff + footguns clean; staging E2E 202.
2026-08-27 15:04:02 +10:00
hermes-seaeye[bot] 36b0a96dcb fmt(js): npm run fix on merge (#96076)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-27 04:43:58 +00:00
Ben Barclay 151632c333 Merge branch 'main' into feat/telemetry-exporter
387 commits from main; no conflicts (verified with merge-tree before
merging). Overlap limited to hermes_cli/config_defaults.py and
hermes_cli/setup.py, both auto-merged; all shared-metrics surfaces
untouched by main.
2026-08-27 14:42:41 +10:00
Teknium ff03d46eef test(desktop): method-aware gateway mock for the refresh-reconcile confirm interaction test
The reconcile-to-guarded-model interaction test's requestGateway mock
must only answer config.set with the confirm handshake — the panel's
model.options read rides the same dispatcher and was eating the
first mocked response.
2026-08-26 21:38:41 -07:00
墨綠BG 477053538c 🐛 fix(desktop): include route scope in gateway dial errors 2026-08-26 21:38:41 -07:00
1052326311 a8169f3e65 fix(desktop): preserve group turn reason codes 2026-08-26 21:38:41 -07:00
Teknium 61b6788dd4 fix(desktop): Bots-mode picker routes guarded model switches through the shared confirm handler
The Bots editor's model write (profiles.configure) was the one switch
surface that bypassed the data-policy / expensive-model selection guard:
a guarded pick (e.g. muse-spark contributor tier) was applied silently,
with no confirm flow anywhere — the #95293 remainder after the core
picker's confirm handshake landed in use-model-controls.

Gateway: profiles.configure now answers confirm_required +
confirm_message for a guarded model (same handshake as config.set
model) and writes NOTHING until the client resends with
confirm_expensive_model: true. Other sections still apply; the pending
model section is not reported as failed.

Desktop: the confirm flow is extracted out of use-model-controls into
one shared applier (lib/guarded-model-switch.ts, exported through the
plugin SDK) — warning toast, staleness-guarded Confirm, single
confirmed resend, never a retry loop. The core picker and the Bots
editor now consume the SAME handler; the Bots editor's Confirm resends
only the model section with confirm_expensive_model: true.

Fixes #95293 (Bots surface remainder).
2026-08-26 21:38:41 -07:00
rainbowgits 0fa98d6a1b fix(desktop): switch model after refresh when it leaves the catalog
Refresh Models only updated the catalog cache, so the composer kept showing a model that was no longer in the new group list.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-26 21:38:41 -07:00
Finn763 911978da62 fix(desktop): Bot Mode model picker always settles and stops remount churn (#95279)
The Bots model picker's catalog read rode the bot's own socket with no
deadline: a wedged dial left the query pending forever, so the picker
spun indefinitely. On top of that every fetch forced refresh:true,
bypassing the staleTime cache, so each Bots view remount (tab re-front,
dialog reopen, pane visibility flip) knocked the picker back into its
loading state and wiped the staged provider/model pick mid-edit.

Bound every attempt to 20s (rejection falls through to the picker's
existing free-text fallback), drop the forced refresh so the read
participates in the cache like every other surface's catalog, and pin
the contract with a red->green regression suite.
2026-08-26 21:38:41 -07:00
Teknium bc4eea7773 feat(desktop): Managed updates section drives per-connection SSH updates
Slim renderer UI for the managed SSH remote update engine (#95942),
adapted from #93042's renderer unit with the deferred canary/rollout
scope stripped. Adds a per-connection store (idle/updating/terminal
states, receipt, managed-update-in-progress busy envelope) and a
'Managed updates' section on the Gateways settings page with an Update
button, progress line, and correlated receipt per registered
Desktop-managed SSH connection. Fails closed when the Electron main
lacks connections.updateManaged.
2026-08-26 21:38:21 -07:00
Finn763 00988f3b89 fix(desktop): widen pool keepalive-fresh window to absorb WSL2 IPC stalls (#95189)
The renderer pings each pool backend every 60s (`hermes:backend:touch` →
`touchPoolBackend` → updates `lastActiveAt`). The LRU eviction cap used a
keepalive-fresh window of 90s — only 1.5× the ping cadence — to decide
whether a backend was "plausibly still alive". One missed or delayed ping
pushed a live backend past the threshold and the cap-driven eviction killed
the active profile's backend mid-session, restarting the gateway and
re-minting runtime ids. On WSL2, where the renderer→Electron IPC roundtrips
through 9p, brief 9p hiccups commonly stretch a ping to seconds of observed
silence, producing the ~80–90s exit / ~2 min cycle reported in #95189
(122 gateway starts on 2026-08-26 alone, driving renderer OOM via reconnect
churn at ~5GB/day).

Widen POOL_KEEPALIVE_FRESH_MS to 4 minutes (3× ping cadence + IPC stall
headroom, still bounded well below POOL_IDLE_MS=10min). Backends with one or
even two missed pings are now spared; truly idle backends (multiple lapses,
minutes idle) remain eligible for eviction by the cap and the idle reaper.
The constant is also overridable via HERMES_DESKTOP_POOL_KEEPALIVE_FRESH_MS
to make this tunable without a rebuild.
2026-08-26 21:38:20 -07:00
Cursor Agent 8246c4f92a fix(cli): repair interrupted update fleet restart
An interrupted hermes update after git pull advanced HEAD never
restarted running gateways, and the next update said "Already up to
date" and skipped the fleet. Persist a HERMES_HOME fleet_restart_pending
marker after HEAD moves, clear it only when restart completes (or
nothing was running), and catch up on the next hermes update even when
git is current — also when latest.json records a stale runtime SHA.

Co-authored-by: GokayAI <gokay-ai@users.noreply.github.com>
2026-08-26 21:38:20 -07:00
lesseradmin 1341dfbd12 fix(compaction): exclude operational notifications from tail anchor and auto-focus (#92703)
Kanban/background completion wakes persist as role=user rows typed with
display_kind="internal_notification" (the synthetic-wake path in run.py).
The model-payload builder already strips display_kind before the request
and is_user_originated_turn already ignores it, but two compaction scans
still treated those rows as real user turns:

- _is_actionable_user_turn (tail anchor) only checked role/content, so a
  notification became the protected 'last user turn' the compressor keeps.
- _derive_auto_focus_topic only skipped synthetic compression turns, so
  operational notices leaked into the compact focus hint.

Both now exclude display_kind-typed rows, mirroring the existing
is_user_originated_turn exclusion. No schema change; cache- and
role-alternation-safe.

Behavior-contract tests feed 1,000 operational notifications around one
human turn and assert they never anchor the tail, become the auto-focus
source, or count as actionable user turns.

Fixes #92703
2026-08-26 21:38:20 -07:00
rainbowgits 11cf59d9a5 fix(tui): adopt live compression config on the next Desktop/TUI turn
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-26 21:38:20 -07:00
fangliquanflq cb54576b1a fix(gateway): isolate control routes from default executor 2026-08-26 21:38:20 -07:00
Teknium 26e48dd8c3 chore: map contributor email for kvnloo (#95173) 2026-08-26 21:38:20 -07:00
Teknium 2c495acb83 test: flush the prune-lease dial by condition, not by hop count
#95343's withTimeout wrapper added an await hop to openSecondary's dial,
breaking the sibling test's exactly-two-Promise.resolve flush. Condition-
bounded flushing survives future hop-count changes.
2026-08-26 21:38:00 -07:00
686f6c61 21054b751e fix(desktop): hide unreachable same-name Bot Mode roster twins
Group rooms persist source-qualified members. After Desktop switches to
the built-in This-device source, a dead loopback row still listed next
to the live profile and looked like a second agent. Collapse only the
sidebar tiles; $lastRoster, group seats, and mentions keep every
(connectionId, profile) identity.
2026-08-26 21:38:00 -07:00
kim-miram ac2ec56833 fix(desktop): retain local startup profile across SSH switches 2026-08-26 21:38:00 -07:00
Teknium 0cefc491c8 test: widen the update-all scan window for the salvaged #95606 wiring pin
main's hermes:connections:update-all handler grew (renderer-side exclusions +
the managed-SSH dispatch branch) after #95606 branched, pushing the claim-
guarded dial past the 2,000-char slice.
2026-08-26 21:38:00 -07:00
nftpoetrist 1e73281614 fix(desktop): claim-guard every remaining ensureRegistryBackend()/ensureBackend() call in Electron main
(connectionId, profile) scope, but it was only wired into hermes:connection,
hermes:connection:for, and the power-resume pool rebuild. Five other real
call sites still invoke ensureRegistryBackend()/ensureBackend() directly:
the media-protocol connection resolver, the terminal-pane backend resolver,
the ~5s roster-enumeration probe, the connections update-all dispatch, and
dispatchRegistryApiRequest (every registry-scoped hermes:api REST call).

ensureRegistryBackend() has a genuine await-then-check race
(reuseMatchingPrimarySshBackend before the pool entry check-and-set), so any
of these five racing a guarded dial for the same scope can each bootstrap
their own SSH tunnel / remote dashboard for the same connection — exactly
the symptom class #90812 was written to prevent, just reached through an
unguarded path instead of two renderer windows.

Left the ensureRegistryBackend() self-call inside its own dispatch-time
health-probe reconnect branch (registryDispatchRevalidation) unguarded —
that recursive path has its own coordination semantics and touching it
without modeling reentrancy against the newly-guarded entry points here is
a separate, riskier change better done on its own.
2026-08-26 21:38:00 -07:00
Finn763 b1f133d4ea fix(desktop): defer gateway liveness force-close while a turn is in flight (#95327)
A wake-path liveness-probe timeout force-closed the primary renderer
socket even when the backend was merely busy mid-tool-call; the gateway
then saw its client vanish, ws_orphan_reap expired, and the running turn
died as a bare "Operation interrupted." placeholder.

While any session still reports working, the first inconclusive probe
timeout now defers the teardown behind one bounded re-probe; only an
exhausted consecutive-failure streak (or no in-flight work at all)
rebuilds the transport. The streak resets on a successful ping, a
healthy -32601 answer, a clean open, a gateway switch, and unmount.
2026-08-26 21:38:00 -07:00
nftpoetrist fbe51fa734 fix(desktop): bound the onActiveConnectionInvalidated fallback getConnection() call
The registry's active-connection-invalidated fallback re-dial was the one
getConnection() await in this file the #93454 bound-every-IPC-round-trip
sweep never reached. A wedged main-process round-trip during an eviction
fallback (idle reap, connection removal, profile delete) left $connection
latched on a promise that never settles instead of rejecting into the
existing catch/publish(null) path.
2026-08-26 21:38:00 -07:00