Commit Graph

4710 Commits

Author SHA1 Message Date
teknium1 9c689bee1a fix(bot-mode): an empty member seat surfaces an error instead of swallowing the group send
Ported from the pre-TSX PR onto the current module layout. main's
group-chat-view.tsx already restores the composer draft when
sendToGroupChat returns null (feat(desktop): retain Bot group drafts by
room, b42d8279ed), so the draft-loss half of the original fix is
FIXED_ON_MAIN and not re-applied here. What remained: sendToGroupChat in
group-rounds.ts still folded "no text" and "no members" into one silent
`return null`, so a fully typed message into a room whose roster had not
hydrated (or a legacy room record without member descriptors) was
rejected with no thread, no log entry and no error.

The guards are split: empty content stays silent, an empty member seat
raises host.notify with a Bot Mode i18n string (en/ja/zh/zh-hant). The
wording no longer promises a retry will help (review: a legacy room with
no member descriptors never recovers by retrying) and points at the two
real remedies.

The original source-regex .mjs tests are dropped per review; the
contract is pinned by a behavioural vitest in group-rounds.test.ts that
drives sendToGroupChat through the scripted room harness: empty members
→ null + one error toast + no log entry; blank text → null, no toast.
2026-09-15 04:14:09 -07:00
teknium1 81e89ad1c1 test(desktop): kanban block-loop title assertion follows the plain-language copy 2026-09-15 03:50:00 -07:00
teknium1 66878996dd fix(ux): plain-language, actionable user-facing messages (desktop-tui)
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 03:46:45 -07:00
teknium1 ca5822b5a5 test(desktop): mark the loud scope note semantically instead of by class
Asserting the loud variant via the Tailwind `font-medium` class couples
the test to styling: any restyle of the accent breaks it without the
behaviour changing. Give the note role="status" (it announces which
profile edits land in) and a `data-scope-loud` flag for the non-default
variant, and assert those. Also fixes the padding-line lint warning in
settings-scope.test.ts.
2026-09-15 03:42:20 -07:00
teknium1 d85aaa3049 test(desktop): stub the new settings-scope atoms in the ConfigSettings test
config-settings.test.tsx mocks @/store/settings-scope with a hand-written
subset of exports; SettingsProfileScope now also reads
$settingsScopeProfile and $settingsScopeEditsNonDefault, so the partial
mock threw at render time in CI (JS & TS checks red).
2026-09-15 03:42:20 -07:00
teknium1 d7d9209795 fix(desktop): derive the loud scope note from the shared settings-scope store
Rewrite of the original component-local `editingNonDefault` computation
onto the existing scope architecture (owner's ask: "use the same
existing architecture" as the Capabilities/toolset scope handling).

- store/settings-scope.ts gains `$settingsScopeEditsNonDefault`, a
  computed over `$settingsScopeProfile` × `$profiles`, so "the settings
  pages are editing a non-default profile" is a store fact any surface
  can subscribe to, not a per-component recomputation.
- profile-scope.tsx reads `$settingsScopeProfile` for the selected chip
  (instead of re-deriving `override ?? active`) and the new selector for
  the loud/quiet note. User-visible outcome is unchanged: accented note
  for any non-default target, override or not; quiet note for an explicit
  override onto the default; nothing when following the active default.
- Review: an unloaded roster (no `is_default` entry yet) used to suppress
  the note exactly at the landing moment where a bot may already be the
  active profile. The selector now assumes the root profile's canonical
  key as the default, so an unknown default fails loud, not quiet. The
  four-cell truth table is documented at the JSX conditional.
- settings-scope.test.ts pins the selector across active-profile,
  override and unloaded-roster inputs; the component tests from the
  original PR are kept as-is and still pass.
2026-09-15 03:42:20 -07:00
Teknium b047b5db4c fix(desktop): settings pages state loudly when they edit a non-default profile's config
After any Bot Mode chat the active gateway profile is the bot's, so the
settings scope silently followed it — edits landed in
profiles/<bot>/config.yaml with only a faint chip tint as the tell
(#89190/#89162/#89597 report class, live-repro'd: Max Agent Steps written
to scout's config). The applies-to note now renders for ANY non-default
target, override or not, accented; default-profile editing stays quiet.
2026-09-15 03:42:20 -07:00
teknium1 8b9df066e2 test(gateway): trim block-loop wording coverage to two invariants; CLI keeps the needs_input distinction
The salvaged parametrized set collapsed to one neutral case (transient) plus
the needs_input positive control. hermes kanban block now mirrors the
notifier: 'needs a human decision' only when the block was typed
needs_input, 'orchestration attention needed' otherwise.
2026-09-15 03:42:00 -07:00
KoNit-K 5c970d9745 fix(kanban): neutral block-loop wording on the CLI, Desktop toast, wake text and docs
The sibling surfaces of the gateway ping rendered the same false claim:
`hermes kanban block` said "needs a human decision", the Desktop toast title
said "needs a decision", the wake status line (locales/*.yaml
gateway.kanban.wake.block_loop_detected) said "needs a decision" and the
docs described the triage route as "for a human decision". A repeated-block
circuit breaker only establishes that orchestration attention is needed.

Surface sweep from PR #111131 (notifier/test hunks dropped in favour of the
typed-kind formatter from PR #111132).
2026-09-15 03:42:00 -07:00
teknium1 0468acdce6 fix(desktop): key voice-fields autosave on the profile scope string
Parents (profile-config.tsx) build the `profile` scope as a fresh object
literal on every render, so `useMemo(..., [profile])` produced a new
cache writer each time and the autosave effect, which depends on both,
tore down and re-armed its 550ms timer on every unrelated re-render.
Key both on profileScopeKey(profile) — the same string the query cache
already uses as the scope's identity — so only a real scope change
re-arms them.
2026-09-15 03:41:04 -07:00
teknium1 ab1ef0c88e test(desktop): pin that a scoped Capabilities TTS panel saves into its scope
Review on the PR: the load-bearing direction (profile B's scope
forwarded into saveHermesConfigRecord) was only covered by the live
E2E; the unit test asserted just the unscoped default. Renders the panel
with profile={profile:'scout', connectionId:'gw-2'} and asserts the
autosave carries exactly that scope. The @/hermes full-replacement mock
gains profileScopeKey, which use-config-record reaches when a scope is
present. Sabotage (drop the profile prop from <VoiceProviderFields>)
fails this test with `expected undefined to deeply equal {profile:
'scout', …}`.
2026-09-15 03:41:04 -07:00
Teknium bdac5c8e2e fix(desktop): Capabilities TTS voice fields write the scoped profile's config, not the active one
ToolsetConfigPanel threads its profile scope into every fetch but rendered
VoiceProviderFields without it, and the fields were hard-wired unscoped —
configuring profile B's TTS from the Capabilities selector read and
autosaved profile A's whole config record. New capability-scoped
saveHermesConfigRecord (symmetric with getHermesConfigRecord), profile
prop threaded through, per-scope cache write-through. Unscoped callers
(Settings → Voice) unchanged.
2026-09-15 03:41:04 -07:00
teknium1 e860b8e4e4 fix(context): compute-host /context and session.context_breakdown carry the per-file manifest; report blocked files
Why: the tui_gateway live formatter (`_format_live_context_output`, used when
the session runs on a compute host) renders its own summary and never got the
"Context files" block, and `session.context_breakdown` had no structured rows,
so Desktop's popover could not show them. The formatter now appends
render_context_file_lines() with the session cwd bound (the RPC thread has no
session context, so the discovery walk would key on the backend's cwd), and
the RPC payload gains a `context_files` list (contract + generated TS/OpenRPC
+ Desktop type). The docs sentence is scoped to the surfaces that render it.

A file whose content _scan_context_content replaces with a BLOCKED marker was
reported "loaded"; the manifest now runs the same scan and reports `blocked`.
The module docstring names the frontmatter-strip / chain-cap approximations
and drops the product-name attribution (credit stays in the PR body).
2026-09-15 03:37:49 -07:00
kshitijk4poor e112da7578 docs(desktop): preflight comments describe the OAuth-only branch; scope assertion via objectContaining 2026-09-15 16:07:43 +05:30
kshitijk4poor 2821cb2d8c refactor(desktop): one profile-order sort for the active strip and the at-rest groups
sortByProfileOrder moves to a pure lib module with a key selector so
buildRestGroups sorts its named squares directly, replacing the collator
sort that the component then re-sorted with a different comparator. The
fleet rail test no longer needs importOriginal (and three store mocks) to
reach the helper.
2026-09-15 16:07:43 +05:30
kshitijk4poor 8751b3edd4 test(desktop): keep the unscoped getProfiles invariant separate from the scoped one 2026-09-15 16:07:43 +05:30
kshitijk4poor 9bb985c5d4 fix(desktop): OAuth REST preflight gets its own dial budget
Sharing the remainder of SWITCH_DIAL_TIMEOUT_MS meant a slow-but-successful
socket dial left the preflight 0 ms and the switch failed as "Timed out
connecting" although the socket had just opened.
2026-09-15 16:07:43 +05:30
kshitijk4poor b591df7c42 fix(desktop): at-rest rails read the roster only; active gateway keeps its single render path
The renderer's per-connection list no longer feeds buildRestGroups: with no
roster reconciler it could outlive a profile deleted elsewhere. The active
gateway is never rendered through FleetRestGroup — that path routed its own
squares through selectConnection (full dial + wipe) instead of selectProfile's
live swap. What survives from the original change is the order parity: at-rest
named squares follow $profileOrder like the active strip.
2026-09-15 16:07:43 +05:30
kshitijk4poor 256edfc1f5 fix(desktop): profile-list ownership follows the published source, without a roster reconciler
The per-connection list cache is kept only to repaint $profiles on re-home.
The $fleetRoster listener is dropped: a roster landing while the active
source's own /api/profiles read was in flight invalidated that read, so
$profiles stayed empty/stale after every switch or focus refresh that the
roster IPC won. Both are reads of the same backend; neither is "older".

A null descriptor is a reconnect blip (setConnection's contract) and keeps
the current owner instead of blanking the rail; the first published
descriptor adopts whatever list is already loaded. Legacy sources are keyed
by endpoint rather than a JSON tuple. Tests cover the roster race and the
null blip; the two use-session-actions tests now publish the descriptor
before seeding $profiles, matching the runtime order.
2026-09-15 16:07:43 +05:30
Zeus-Deus 5b49051276 fix(desktop): keep profile switches on the selected gateway 2026-09-15 16:07:43 +05:30
kshitijk4poor 8bdac1b17a fix(desktop): omit a null iss from the oauth.callback relay; hoist the loopback parse import
The Electron listener now always emits iss (null when the server sent none)
and McpOauthCallbackParams is extra="forbid", so a new Desktop against a
backend without this change would fail every remote MCP OAuth login with a
4000 - including providers that never send iss. Send the key only when set.
Also drop the deliver_callback_flow test the RPC test subsumes.
2026-09-15 13:00:12 +05:30
kshitijk4poor 1c243f86de fix(tui_gateway): relay RFC 9207 iss through the oauth.callback RPC
The oauth.callback handler parsed `iss` but never passed it to deliver_callback_flow, and McpOauthCallbackParams (extra="forbid") had no `iss` field, so the desktop renderer sending `iss: null` was rejected with 4000 "unknown key" — breaking every Desktop→remote-gateway MCP OAuth login. Add the field, forward it, and regenerate the OpenRPC/TS contract artifacts via scripts/gen_gateway_contracts.py.

Also update tests/hermes_cli/test_mcp_dashboard_oauth.py for the 3-tuple callback shape introduced by the cherry-picked commit (it was red on the stack).
2026-09-15 13:00:12 +05:30
OOOOOAO 1a6503a520 fix(mcp): thread RFC 9207 iss through every OAuth callback relay
mcp 2.x rejects an authorization response that omits the RFC 9207 `iss`
parameter when the authorization server advertised
`authorization_response_iss_parameter_supported`. Cloudflare advertises it
AND sends it; the CLI loopback handler has always forwarded it, but every
other callback producer parsed only code/state/error, so the SDK raised:

    OAuthFlowError: Authorization response missing iss parameter
    advertised by the authorization server

and the server parked. Same machine, same config, `hermes mcp login <name>`
from a terminal succeeded — the failure is specific to the non-CLI relays.

Forward `iss` on every producer, matching `_make_callback_handler()`:

- tools/mcp_dashboard_oauth.py: `deliver_callback()` accepts `iss`;
  `wait_for_callback()` returns `(code, state, iss)`. The bridge in
  tools/mcp_oauth.py already splats that tuple into
  `_authorization_code_result(code, state, iss)`, so it needs no change.
- tui_gateway/mcp_oauth_sessions.py: the gateway-hosted loopback listener
  parses `iss`, and `deliver_callback_flow()` forwards it.
- tui_gateway/methods_tools.py: the `oauth.callback` RPC passes `iss`.
- hermes_cli/web_routers/mcp.py: the dashboard callback route accepts it.
- apps/desktop/electron/mcp-oauth-callback-ipc.ts: the one-shot listener
  reads `iss` off the redirect (the renderer already spreads the whole
  callback object into the RPC, so it flows through unchanged).

Providers that omit `iss` round-trip as `None`/`null` rather than being
dropped, so servers that do not advertise RFC 9207 keep working.

Verified live on Windows against mcp.cloudflare.com, whose metadata sets
`authorization_response_iss_parameter_supported: true`: the server that
previously parked on the missing-iss error now reports
`Authenticated — 3452 tool(s) available` and `hermes mcp test cloudflare`
connects. State-mismatch and replay rejection are unchanged.

Tests (each fails on base, passes with the fix):
- test_dashboard_flow_preserves_rfc9207_iss
- test_deliver_callback_forwards_iss (client-redirect relay)
- test_loopback_listener_forwards_iss (real HTTP redirect)
- two vitest cases on the Electron listener, incl. the iss-absent case

Refs #92758, #99984. PR #92765 fixes the dashboard route and the loopback
listener but not the client-redirect relay
(`deliver_callback_flow` / `oauth.callback` / the Electron listener), which
is the path Desktop drives against a remote backend.
2026-09-15 13:00:12 +05:30
brooklyn! d128ce2e25 fix(desktop): keep sudo commands visible before password entry 2026-09-15 02:22:22 -05:00
brooklyn! b79107c565 fix(gateway): include command context in sudo password requests 2026-09-15 02:22:22 -05:00
Austin Pickett 5871d750bf test(bot-mode): pin thread-scoped session identity
The room-level test is the reported defect: two composer sends, two
threads, and neither backend transcript may contain the other thread's
prompt. Proven red on the unfixed code — both threads resolved to
sid-research-1.

Also pins same-thread continuity, the pre-thread adoption (first thread
continues the old conversation, later threads do not), and that the
member half of the key stays source-qualified so a remote `research` and
a local `research` never share a session.

Co-authored-by: Wenfengcheng <30426178+Wenfengcheng@users.noreply.github.com>
2026-09-15 02:12:21 -05:00
Austin Pickett 1ce9733113 fix(bot-mode): scope stop, harvest and clarify to the originating thread
The co-keyed readers of `room.sessions` had to follow session identity or
they would split from it: the stop interrupt targeted a member's only
session regardless of which thread issued the stop, and the stranded-reply
harvest resumed by bare member key even though its marker already carries
`{before, thread}`.

The clarify mirror moves too — with per-thread sessions one member can be
blocked in two threads at once, and a room-and-member key let thread B's
question silently replace thread A's card. The sweep's owner lookup stays
a MEMBER question, so it reads the member half of the key and keeps the
`::` source-qualifier check on that half alone.

Co-authored-by: Wenfengcheng <30426178+Wenfengcheng@users.noreply.github.com>
2026-09-15 02:12:21 -05:00
Austin Pickett d631ad5f5c fix(bot-mode): key group member sessions by thread
A group member's hidden plumbing session was keyed by member alone, so
every thread in a room collapsed onto one backend transcript: thread B
resumed thread A's session and answered carrying A's context.

Everything else in the room engine is already thread-aware — the delta is
filtered by `groupThreadOf(e) === thread` and the watermark is
`${thread}::${memberKey}` — so only session identity was missing. Key and
title it per thread, and adopt a room's pre-thread pointer into the first
thread that speaks so an upgraded room's history is continued rather than
orphaned.

Co-authored-by: Wenfengcheng <30426178+Wenfengcheng@users.noreply.github.com>
2026-09-15 02:12:21 -05:00
kshitijk4poor db64ddb58e test(desktop): widen the execProbe event-loop test timeout
The 'execProbe keeps the parent event loop available' case asserts nothing
about timing; the 1s spawn budget only exists so a wedged child cannot stall
the run. A cold Windows CI runner can take longer than 1s just to start the
node child, which failed the test for reasons unrelated to what it guards.
5s keeps the safety bound without the flake.
2026-09-15 10:50:53 +05:30
kshitijk4poor 6915e9b4bc fix(desktop): keep the pool slot leased when a spawned start is superseded
runPoolBackendStart reused assertPoolEntryStillOwned at every cancellation
checkpoint, and that helper releases the local backend slot before it throws.
That is right before spawn (no child, nothing to wait for) but wrong at the
post-spawn checkpoints (after the port announcement, waitForHermes, token
adoption, WS probe): the lease was handed back while the superseded child was
still running, so a successor could spawn into an occupied slot, and the
caller's teardownFailedLocalBackend -> releaseLocalBackendSlotAfterExit then
found nothing left to release and became a no-op. This breaks the
pool-spawn-coordinator invariant that a lease is held until the child exits
or the start fails.

Add a `releaseSlot` option to assertPoolEntryStillOwned and pass
`{ releaseSlot: false }` at every site where `entry.process` is set. The
pre-spawn sites keep releasing. The post-spawn release now happens only via
teardownFailedLocalBackend (after the child has provably exited) or the
child's own exit handler.

No vitest case: the helper and its callers live in main.ts alongside the
module-level backendPool / localBackendLifecycle state and are not importable
from a unit test without extracting them; the lease-after-exit ordering
itself is already pinned by pool-spawn-coordinator.test.ts ('a rejected wait
keeps the slot occupied').
2026-09-15 10:50:53 +05:30
kshitijk4poor 844e5f2610 fix(desktop): do not cache a timed-out serve-support probe
The serve-support resolver caches the probe outcome per resolved runtime
for the process lifetime. That is right for a genuine "unknown
subcommand" exit, but a probe that died by timeout says nothing about the
runtime — only that this machine was slow right then (cold AV scan on
Windows, first Python import after boot). Caching that as `false` routed
a modern runtime through the legacy `dashboard` form until the app was
relaunched.

Export isTimeoutError from backend-probes and evict the cache entry on a
timeout so the next check re-probes; non-timeout failures stay cached.
One vitest case covers timeout → re-probe; the existing case still pins
non-timeout failure → cached.

Also replace the last synchronous read on the discovery path
(dashboard.py fast path) with fs.promises.readFile, matching the stack's
goal of keeping runtime discovery off the main event loop.
2026-09-15 10:50:53 +05:30
kshitijk4poor 09cf145ec8 refactor(desktop): tighten runtime-discovery async and attempt guards
isActiveRuntimeUsable relied on async-return flattening for the trailing
canImportHermesCli promise: correct today, but appending any further `&&`
operand would have made the expression truthy regardless of the probe.
Await the probe explicitly so the intent survives future edits, and mark
unwrapWindowsVenvHermesCommand async like its siblings since it always
returns a promise.

connectRemote used two inline isCurrentAttempt/throw pairs with the same
message that backendConnectionState.assertCurrentAttempt already emits,
and the third guard in the same function already uses the helper. Use it
in all three places so the superseded-attempt message has one source.

The fast-path/probe/cache explanation for serve support lived above the
resolver call site in main.ts while the logic lives in
backend-serve-support.ts; move it next to the code and leave a pointer.
2026-09-15 10:50:53 +05:30
jango 3952878c2a [verified] test(desktop): tolerate loopback peer reset
(cherry picked from commit fcbadf23c95d153b8a1f8d68fc531734050fadfe)
2026-09-15 10:50:53 +05:30
jango 5d3cb88dbc [verified] test(desktop): guard nonblocking runtime probes
(cherry picked from commit ee61bbfa311a0a99a1f8c14c18e3cdc97e51f3d1)
2026-09-15 10:50:53 +05:30
jango 783a06d070 [verified] test(desktop): trim runtime probe coverage
(cherry picked from commit 83b7fed46834988f66051f73dab5c65dbf47a672)
2026-09-15 10:50:53 +05:30
jango fa0f1986dc test(desktop): verify actual Electron browser identity across platforms
(cherry picked from commit 6cd0a15ed98ced17fe1bf1d6e81e2201aefc1fd2)
2026-09-15 10:50:53 +05:30
jango e270766fe5 test(desktop): detect transient stale spawns and native path separators
(cherry picked from commit 807ef07f71a3ddceb63c8ee1d688739ab6ab9d62)
2026-09-15 10:50:53 +05:30
jango 78399c88da test(desktop): verify probe responsiveness and stale-start cancellation
(cherry picked from commit c12f7e0f677a234e7e5057a13485e39346da0d03)
2026-09-15 10:50:53 +05:30
jango 5d9f83253f fix(desktop): keep runtime discovery off the main event loop
(cherry picked from commit d116c193ca790331acda9ebf594931ac4033e0fd)
2026-09-15 10:50:53 +05:30
kshitijk4poor 45760ac022 fix(desktop): match app.asar segment on either separator in resolveOutsideAsar
The redirect out of the archive used a literal '/app.asar/' replace, so the
staged get-windows specifier built with path.join on a Windows packaged build
(...\resources\app.asar\dist\...) was never rewritten and the helper spawn
would still land inside the archive. Mirror the app.asar(?=$|[\\/]) regex
main.ts already uses for the same job, and cover the backslash path in the
test. The dev-tree no-op case folds into the segment-boundary test so the
describe block stays at three cases.

Closes #88468 (with the preceding commit from PR #89633).
2026-09-15 10:49:40 +05:30
Douglas Jennewein 763489a8b4 fix(desktop): exec get-windows helper from app.asar.unpacked so read_window_below works on packaged macOS builds
`read_window_below` answers "could not enumerate windows on this system" on
every packaged macOS build. get-windows does its real work by exec'ing a Swift
helper whose path it derives from its own module URL, and execFile cannot run a
file that lives inside app.asar: the kernel sees the archive as a file, so the
spawn fails ENOTDIR. Electron does rewrite asar paths for child_process, and
only once that module has been pulled through the CJS loader, which this ESM
main process never does (every import here is `from 'node:child_process'`).

Both specifiers now resolve outside the archive. The staged copy added in
1c75e05982 is redirected, and so is the node_modules fallback, which needs
`import.meta.resolve` first to have a path to redirect. electron-builder
unpacks the whole get-windows directory, so the redirected path exists on the
real filesystem and the derived helper path is executable.

`/app.asar/` only ever appears as a complete path segment in a packaged build,
so outside one the replace is a no-op. Tests cover the packaged redirect, the
dev-tree passthrough, and an `app.asar-tools` lookalike that must be left
alone.

Verified on a packaged build, macOS 26.6.2 arm64, ad-hoc signed: the same
machine that returns the enumeration failure on unpatched main answers with the
window underneath and its title.

(cherry picked from commit dce34f626a0796fcd945aa3885384a1ec86c819f)
2026-09-15 10:49:40 +05:30
Jeffrey Quesnelle d08032655f Merge pull request #111423 from NousResearch/feat/local-engine-update-prompt
feat(local-models): prefer b10964 and add one-click engine updates
2026-09-14 22:25:46 -04:00
emozilla c89b1d1318 test(desktop): complete SDK gateway routing mock 2026-09-14 22:12:53 -04:00
emozilla de5a1da632 feat(desktop): add one-click local engine updates 2026-09-14 21:49:14 -04:00
teknium1 2179a279ae fix(desktop): dragging a link from a browser attaches an @url chip
Dropping a link out of a browser onto the Desktop composer toasted
"Drop files — Could not attach <title>.url" and attached nothing. A browser
link drag carries `text/uri-list` plus, on Windows, a virtual `<title>.url`
shortcut File (`.webloc` on macOS) that has no on-disk path. The drop
pipeline only understood Files and in-app paths: the path-less stub went to
the upload branch, `attachContextFilePath('')` returned false, and the user
had to copy/paste the URL instead.

`extractDroppedFiles` now reads `text/uri-list`, drops the path-less
shortcut stub when a link is present, and emits `{ url }` entries;
`droppedFileInlineRef` turns them into the same `@url:` chip the "+ → Add
URL" dialog and paste-linkify produce, so every drop surface (composer
form, text box, conversation area, edit composer) gets it for free.
`dragHasAttachments` accepts `text/uri-list` so the form-level enter/over
handlers claim the drag at all. A path-less *image* dragged off a web page
keeps its bytes and wins over the link to its own src.
2026-09-14 18:06:05 -07:00
teknium1 9326d9cdc4 fix(bot-mode): show the current thread and newest unresolved failure
Attribute drive-level errors to the thread being drained, not the send
that created the queue. Reinsert repeat member failures in recency order
so the collapsed activity row cannot show an older sibling failure.

Cover both invariants and the repeated-refusal sequence in native Desktop.
Thanks to @kvnloo for identifying both review findings.
2026-09-14 17:35:04 -07:00
teknium1 3040c87ac1 fix(bot-mode): do not retry failures from already queued sends
Keep failed-member exclusion across the room queue, rather than resetting
it per pending thread. A new user action after failure still permits a new
attempt. Share the drain activity epoch so a skipped queued thread cannot
hide the preceding member failure.

Proven red in real Electron: hold transport refusal, enqueue same-thread
and cross-thread sends, then release; old head submits three times, fixed
head once. Strengthen follow-up evidence with distinct provider replies,
exact public log order/count, and per-input inference counts.
2026-09-14 17:35:04 -07:00
teknium1 e80642df60 fix(bot-mode): keep group follow-ups ordered and late answers visible
Serialize room drives through their actual member completion, freeze input
watermarks by retained entry identity, and share the same completion path
with handoff continuations. Stop discards queued work without releasing an
active owner early; rename follows the existing room binding.

Observe stranded replies for the hard-cap duration plus grace after the
foreground wait, and retain unresolved failures in collapsed Activity.
Never automatically retry an ambiguous failed submit within the same drive.

Adapted from the queue and boundary approach in #92041 by @enwaiax and
harvest-budget approach in #107193 by @Finn763; #106502 by @wadib identified
failed-submit watermark consumption. The implementation retains current
numeric watermark storage, room lifecycle bindings and serial round limits.

Related: #92003, #105247, #100026
2026-09-14 17:35:04 -07:00
teknium1 1f1f02e354 fix(desktop): preserve qualified bot identity in primary handoffs
Keep the salvaged botHandle normalization, but remove the unconditional
hermes alias on remote default profiles: the parser's last-wins map
otherwise retargets a local @hermes handoff by roster order.

Consolidate regression coverage into two invariants for persisted primary
handles, both handoff directions and three-source qualified identity.
Capture real Electron screenshots, durable logs and source receipts;
exercise the reverse live handoff too. Document the repair and bump the
bundled Desktop patch version.
2026-09-14 17:04:27 -07:00
teknium1 52972d15f3 test(desktop): live e2e for a teammate's @hermes handoff driving the primary bot
Real-Electron Playwright spec for #100406: a two-member room (primary
profile + code-farmer). The user addresses only @code-farmer; its
scripted reply @mentions hermes; the assertion is a `default`-authored
"B" entry in the persisted room log. On origin/main the room settles
after Code Farmer's line and the spec fails at that assertion; with the
mention-alias fix it passes.

The mock inference server gains a per-speaker script for group rooms:
`E2E_SAY(<handle>)[<line>]` tokens in the user's send answer the member
whose turn prompt opens with `You are @<handle>`; unscripted members
reply "(pass)". `{at}` stands for `@` so the script itself never
mentions anyone and round one only drives the member the user tagged.
2026-09-14 17:04:27 -07:00