Commit Graph

46076 Commits

Author SHA1 Message Date
kshitijk4poor
71fe5fccad refactor(model_switch): remove parse_model_flags, which only tests called
Every /model surface parses through parse_model_switch_args. The legacy
5-tuple wrapper had no production caller; its remaining references were two
tests of the tuple shape, an unused import, and two tui_gateway patch()
calls on a function tui_gateway no longer calls.

The en-dash --session normalization those tests covered moves into the
parse_model_switch_args keeper. test_model_switch_once_flags and
test_model_switch_context_offload are removed: the first re-tests the
private parser the keeper already drives, the second the one-line to_thread
wrapper that tests/gateway/test_model_command_context_offload.py proves off
the event loop through the real /model handler.
2026-09-29 15:58:55 +05:30
kshitijk4poor
9ee791eef1 test: drop tests that cannot fail or duplicate a same-file keeper
Each removed test either asserts nothing reachable or repeats another
test's exact body:

- auxiliary_client empty-base_url: body only writes config.yaml; a merge
  (890bbbda1f) restored the header of a test b3d4f67b20 had purged. The
  non-Anthropic-host test beside it drives the same default fallback.
- background_review no-session-db: _record_review_usage_to_parent swallows
  every exception and there is no DB to observe, so removing the guard
  still passes; the path has no observable effect to pin.
- code_execution Windows "would have failed" pair: every branch returns,
  skips or passes; they exercise CPython defaults, not Hermes code.
- cron test_should_use_direct_api_call_*: byte-identical copy of the test in
  tests/agent/test_cron_inline_api_call.py (0213b8b77b had to edit both).
- cron liveness no-multiplexer: identical body to test_no_lock_no_pids_is_false;
  its multiplexer patch is never reached.
- browser console/get_images "private URLs allowed": identical to the
  local-backend test in each file; both mock the predicate the name promises.
- hermes_state TestTitleUniqueness: the duplicate-title case is a strict
  subset of TestSessionTitleLineage::test_unrelated_session_still_conflicts;
  the NULL-title case never writes a title.
2026-09-29 15:58:55 +05:30
kshitijk4poor
bca2e3c5a4 docs(profiles): say the interactive uninstall menu still offers a full wipe
The stale-profile recovery paragraph claimed uninstall only runs in a
mode that keeps user data. Plain interactive `hermes uninstall` is let
through and its menu still offers Full uninstall of the default root,
behind its confirmation prompts. Say so, and note in the predicate why
--full is refused (it wipes without asking) while the menu is not.
2026-09-29 15:45:52 +05:30
kshitijk4poor
192e574efe docs(profiles): what still runs when the sticky profile was deleted 2026-09-29 15:45:52 +05:30
kshitijk4poor
fbb0eb35e1 fix(cli): missing sticky profile error names the recovery command
When active_profile names a deleted profile, the error still said
"Create it with: hermes profile create <name>", a command that exits with
the same error. Name the command that works instead:
`hermes profile use default`. An explicit -p keeps the create hint.
2026-09-29 15:45:52 +05:30
kshitijk4poor
167d3bb6f3 fix(cli): stale-profile recovery refuses uninstall --full and abbreviated --data
The recovery allowlist checked for the literal string "--data", but the
uninstall subparser accepts argparse abbreviations: `hermes uninstall --dat`
passed the guard and removed the default root's data. `--full` was allowed
outright, and in the default root it rmtree's every other profile under
profiles/ as well, a wider wipe than the --data mode the guard refuses.

Parse the uninstall argv with the real subparser (output silenced; the
handler reports errors itself) and allow it only when neither data nor full
resolves true. `uninstall --help` exits 0 during that probe and is let
through so the handler prints help once instead of help plus an error.
The predicate lives in a topical sibling (main_profile_recovery.py) rather
than growing the main.py facade.
2026-09-29 15:45:52 +05:30
Gille
a4748e9224 fix(cli): allow recovery from missing active profile 2026-09-29 15:45:52 +05:30
unsupportedpastels
536802c00e fix(debug): mask every secret fallback field per agent.redact policy, not only api_key
Reuses the repr-field secret-name policy (is_secret_field_name) so token, password, secret and credential-suffixed fields are masked by field in hermes dump, debug share and /debug. The regression test asserts on what actually reaches the paste service first.
2026-09-29 05:59:25 -04:00
unsupportedpastels
f907520593 fix(debug): mask fallback api_key by field, whatever its YAML shape
Text redaction of str(fallback_providers) missed api_key values that the
runtime still uses (resolve_entry_api_key str()s them): an unquoted numeric
key, a mapping, or a key containing "***" (treated as already masked).
Mask the field before serialising; keep the independent upload-boundary
pass and cover it with #126168's boundary test.

Co-authored-by: John Paul Soliva <soliva.johnpaul@icloud.com>
Co-authored-by: JoaoMarcos44 <joaomarcosdias444@gmail.com>
2026-09-29 05:59:25 -04:00
John Paul Soliva
cf94e3eea3 fix(redact): GCS and Azure SAS signatures count as URL credentials
The strict URL-credential pass that hermes dump and debug uploads use
redacted X-Amz-Signature but not X-Goog-Signature (GCS V4 signed URLs)
or sig (Azure SAS), so a fallback base_url signed that way still reached
the dump and the upload. The documented promise is signed-URL signatures
in general.
2026-09-29 05:59:25 -04:00
John Paul Soliva
139876e727 test(debug): URL credentials in a fallback base_url never reach hermes dump or an upload
Covers userinfo, ?key=, X-Amz-Signature= and #access_token= through the real config.yaml, hermes dump stdout, the share bundle and the /debug report. Docs (EN + zh-Hans) say the dump redacts credentials in fallback URLs.
2026-09-29 05:59:25 -04:00
JoaoMarcos44
205d5d78f1 fix(debug): strict URL-credential redaction for the system dump
A fallback_providers base_url can carry its credential in the URL (userinfo, ?key=, #access_token=, X-Amz-Signature=). The force pass leaves those to the log policy, which spares OAuth/magic-link URLs, so hermes dump printed them and debug share / gateway /debug uploaded them. The dump's fallback rendering and the upload copy of the dump now opt into redact_url_credentials.

The strict canonicalizer also turned x-amz-signature into x_amz_signature before the policy lookup, so strict mode never masked it; hyphenated policy names now match as stored.

Ported from #126168.
2026-09-29 05:59:25 -04:00
John Paul Soliva
e70fbbe3ec fix(debug): redact the system dump in debug share and gateway /debug
hermes dump prints config_overrides.fallback_providers as the raw list,
api_key values included, and the upload-time redaction of hermes debug
share and the gateway /debug report covered only the log snapshots, not
the dump text quoted at the top of every paste.

Force-redact the captured dump like the logs (--no-redact still opts
out), and have hermes dump itself redact fallback_providers so a pasted
dump never carries a key.
2026-09-29 05:59:25 -04:00
kshitijk4poor
ea7c6a1339 refactor(update): one explanation of the tail exemption, with the lock guard reworded
The lock-ancestry comment still claimed it was what stops a tail recursing;
the tail scripts now return before it, so it describes the processes an
update spawns instead. Also note why the identity reads sys.argv[0] rather
than the argv parameter.
2026-09-29 15:25:13 +05:30
kshitijk4poor
4e5cea136a test(update): a same-named script outside the checkout still repairs it
Pins the path half of the tail identity: only this checkout's own
hermes_cli/ scripts are exempt from the launch-time repair.
2026-09-29 15:25:13 +05:30
kshitijk4poor
6c7ebd1c01 fix(update): hermes update's own tail no longer starts a second tail
update_completion.py arms the pending marker, then imports hermes_cli.main,
which runs prepare_launch. While the updater's lock claim is live the
ancestor check returns early, but an unwritable claim or one past the
20-minute ceiling sends it into _finish_source_update: a full
source_completion tail runs inside the update and clears the marker before
the update's own build has finished.

Exempt both tail scripts through one identity check. The launch check
already resolves its root, so the comparison is exact on the process's
own argv[0], which the processes a tail spawns do not inherit.
2026-09-29 15:25:13 +05:30
Gille
de48024d75 fix(update): prevent recursive source completion
(cherry picked from commit 481e68381ebb9bbc36dd7b99876b6fd8b833584a)
2026-09-29 15:25:13 +05:30
kshitijk4poor
596678d665 refactor(update): tighten the partial-clone heal after review
- _deepen_shallow_repo marks packs in its finally without a git config probe
  that could itself raise and turn a finished unshallow into False; markers
  are inert in a clone without a promisor remote.
- Installers warn instead of aborting the stage when a marker cannot be
  written, matching the Python helper.
- Docs: the manual commands follow HERMES_HOME, and the missing-object
  backfill feeds IDs through `fetch --stdin` so a large list cannot exceed
  the Windows command-line limit.
- Test and docstring comments describe the contract, not the incident.
2026-09-29 15:24:15 +05:30
kshitijk4poor
be475657ea docs(update): recover a partial clone stuck on the git pack-objects crash (#124272)
Manual marker one-liners for installs whose own updater cannot fetch the fix,
and the missing-object backfill for checkouts that were already turned into
full clones by hand.
2026-09-29 15:24:15 +05:30
kshitijk4poor
7b20b72269 fix(install): installer rerun heals a partial clone stuck on the pack-objects crash
An install whose updater dies on the git 2.53+ pack-objects BUG (#124272)
never fetches the release that heals it: the fetch runs in the installed
code. The installer rerun (install.sh / install.ps1, served fresh) is the
path that runs new code, so when origin is a promisor remote it now marks
the checkout's unmarked packs before its existing-checkout fetch. Marking is
idempotent and never rewrites objects, so it runs up front rather than as a
retry that would first print a failed-fetch report.
2026-09-29 15:24:15 +05:30
kshitijk4poor
282ad01cb5 fix(update): mark unmarked packs instead of the inert promisor retry (#124272)
git 2.53+ `index-pack --promisor` hands every object outside the promisor
packs to pack-objects, which BUG()s (should_include_obj) on the missing
objects those objects lead to. A partial clone with unmarked packs therefore
keeps crashing its fetches.

The retry added for this ran `git -c remote.origin.promisor= fetch`. That
override does nothing: git registers promisor remotes additively, so the
repo's own `true` still wins (GIT_TRACE on 2.50 and 2.55 shows
`index-pack --promisor` and `filter tree:0` under the override). The retry
only passed when the crashed fetch had already stored the pack; a retry that
has to transfer again crashed again.

- On the crash, give every pack without a `.promisor` marker one, then retry
  the same fetch once. The clone stays partial, no refetch, no config change.
  If git still crashes, the updater points at the docs section.
- The two fetches that turn a full clone partial now mark the clone's old
  packs afterwards: the depth-1 `tree:0` unshallow in
  `fetch_full_commit_graph`, and `worktree_ops._deepen_shallow_repo`
  (`--filter=blob:none`). git writes the partial-clone config before it
  fetches, so this also runs when the fetch fails. Without it, a converted
  depth-1 install crashed every later fetch on git 2.55 (3 of 3 rounds in a
  local repro).
2026-09-29 15:24:15 +05:30
Q
70b9cdafd4 fix(update): recognize the git 2.55 partial-clone pack-objects crash
is_partial_clone_pack_objects_crash() required "index-pack failed" next to the
pack-objects assertion, but git 2.55 builds dropped that string: Homebrew git
2.55.0 (macOS) only reports "fatal: fetch-pack: invalid index-pack output", so
the recognizer always returned False there, fetch_with_partial_clone_recovery()
never retried with the promisor machinery disabled, and `hermes update` exited 1
on the crash (#125138 reports the same bare-assertion failure on 2.55.0.windows.5).

Fingerprint on the assertion text — the only version-stable part of the failure
— and accept the 2.55 wrapper as a terminator. The POSIX/Windows shapes and the
"fingerprint without a terminator" rejection behave as before.

(cherry picked from commit 74103b15ceb3eacb2346fca669c3a3dc81457201)
2026-09-29 15:24:15 +05:30
kshitijk4poor
af26acab73 fix(deps): gate the runtime takeover on genuinely historical updater frames
The salvaged matcher accepted any cmd_update/_cmd_update_impl frame in
hermes_cli.update_cmd/main, so the CURRENT updater's entrypoints (which
carry pre_update_version on opts, not as a local) would also hand off and
exit the process mid-`hermes update`. Extract
_old_updater.in_historical_update() sharing _historical_context's exact
frame matcher (module, name, and pre_update_version in co_varnames) so the
shim and the context detector cannot drift apart.

- tools/lazy_deps: use the shared predicate; accept the constraints/
  dry_run kwargs historical updaters pass so they reach the handoff
  instead of dying on TypeError; drop the vague message advice.
- tests: collapse 29 parametrize cases to the real invariants and add the
  missing one — the frame walk is now exercised through a nested helper
  (a getframe(1)-only implementation fails), and a current-updater-shaped
  frame (no pre_update_version local) is asserted NOT to hand off.
2026-09-29 15:08:07 +05:30
funky-xamarin
5e6165562b fix(deps): refuse runtime lazy installer update takeovers 2026-09-29 15:08:07 +05:30
kshitijk4poor
7728574ad4 test(cli): fold foreign-venv wrapper case into the migration test
The standalone ownership test duplicated the parametrized boot migration
(same wrapper, weaker check), was the only test in tests/hermes_cli calling
the private _publish_conveniences, and asserted a mtime-derived "changed"
flag immediately after writing the file, which is flaky on filesystems with
coarse timestamps.

Keep its one unique invariant, another checkout's venv/bin/hermes is never
adopted, by pointing the migration test's existing foreign hermes-agent
wrapper at another checkout's venv. It still also mentions this install in a
comment, so both rejection cases stay covered through public expose_cli().
2026-09-29 14:36:55 +05:30
Gille
91986da496 fix(cli): adopt legacy venv hermes wrapper
(cherry picked from commit ccdbc32d93358c1d0c8219ab3e9441a8a49fdbee)
2026-09-29 14:36:55 +05:30
Gille
ea114c3e98 fix(install): preserve apostrophe paths in Windows runtime launch 2026-09-29 13:19:27 +05:30
Siddharth Balyan
3a439b69ad Free tier: separate 'account exists' from 'runs this chat', and stop the mint from writing active_provider (#127486) 2026-09-29 07:44:13 +00:00
Siddharth Balyan
666f313d1d Desktop opens new installs at a screen-sized window and 110% zoom (#127448)
* feat(desktop): open new installs at a display-sized window and 110% zoom

A fresh install opened at a fixed 1220x800 and 90% zoom, so body text sat
below the Apple and Windows minimums and users pressed Cmd+ repeatedly.

The first window now takes 75% x 80% of the primary display's work area,
floored at 1220x800, capped at 1600x1000, and never larger than the work
area. A saved window-state.json still wins. The default zoom is the 110%
UI Scale preset; Cmd+0 / Actual Size resets to it. Existing installs keep
their saved zoom (first launch has always written the default to
zoom-state.json).

* chore: retrigger CI (zero-job dispatch failure, auto-heal)

* test(desktop-e2e): let a full-window overlay cover the viewport within 1 px

waitForInteractive treats a fixed element as blocking only when its rect
reaches innerWidth/innerHeight. innerWidth is rounded: at 110% zoom a
1280x860 window is 1163.6 CSS px wide and innerWidth reads 1164, so the boot
overlay (inset-0, 1163.6 px) no longer counted as covering the window and the
update E2E declared the app interactive while it was still booting.
2026-09-29 06:55:56 +00:00
Merge_Conflict - Pasi
39faafb616 fix(desktop): map URL-remote per-profile label to backend profile via remoteProfile (#88282) 2026-09-28 22:27:40 -05:00
Paramon
30a8b539d9 fix(desktop): name pasted composer images with local time, not UTC
writeComposerImage stamped filenames via Date.toISOString(), which is UTC,
so the embedded timestamp disagreed with the file's mtime (shown in local
time) by the user's zone offset - e.g. 8 hours off for UTC+8.

Extract the stamp into composerImageTimestamp() (local wall-clock
components, same YYYY-MM-DD_HH-MM-SS-mmm shape) with regression tests that
distinguish local time from UTC on any host timezone. Machine-facing
artifacts (bootstrap run logs, emergency state.db backups, corrupt-registry
sidecars) keep their UTC stamps intentionally.

Closes #96403
2026-09-28 21:59:27 -05:00
zhengpeng
3cf2eb1c94 test(web): run the dashboard test project in development mode
React 19 only ships `act` in its development build. The jsdom suites in
`web/` drive components through `act()`, so when vitest inherits an
ambient `NODE_ENV=production` — the shell that launched the app, a
packaged CI step, an editor terminal — `React.act` resolves to undefined
and every `createRoot` test in the project fails with
`TypeError: act is not a function`.

Pin the test worker to `NODE_ENV=development` via the project's
`test.env` so the suite no longer depends on the ambient environment.
App runtime and production builds are unaffected; this only applies to
the vitest worker.

Same class of failure as #77409 (and the same fix shape as #77421), one
workspace over: that issue and PR cover `apps/desktop/vitest.config.ts`,
while `web/vitest.config.ts` has no equivalent guard.

Before (clean main, NODE_ENV=production):
  Test Files  3 failed | 33 passed (36)
  Tests      43 failed | 232 passed (275)

After:
  Test Files  36 passed (36)
  Tests      275 passed (275)
2026-09-28 21:54:03 -05:00
chelsealong
ef2013231d fix(gateway): scope the browse-cap exemption to registry commands only
The previous fix exempted every kind != "skill" item from the browse
cap, on the premise that non-skill items only come from the fixed,
~150-entry COMMAND_REGISTRY. That's not true: plugin-registered slash
commands (PluginContext.register_command) are also classified as
kind "command" but are an unbounded, user-installed source — the same
shape the cap exists to bound for skills. A large plugin install would
have flooded a bare `/` completely uncapped.

Only exempt names actually in GATEWAY_KNOWN_COMMANDS (derived from
COMMAND_REGISTRY) from the browse cap; other kind:"command" items,
including plugin commands, keep the existing 30-item cap.
2026-09-28 21:39:39 -05:00
chelsealong
a0ef9f7477 fix(gateway): stop truncating the command block on a bare / browse
_rank_slash_completions applied the same 30-item cap to registry
commands and user-installed skills. The cap exists to bound the
skill block (which can grow to hundreds of installs); the command
registry is fixed and curated (~150 entries). On a bare `/`, that
shared cap put /goal, /subgoal, /loop, /queue, /personality and
/debug past position 30 and hid them from the desktop and TUI
browse list entirely, even though they still resolved by prefix.

Only skip the cap for commands when browsing (text == "/"); a
typed search still bounds both blocks.

Fixes #101388
2026-09-28 21:39:39 -05:00
John Paul Soliva
c2f3192929 fix(desktop): a local profile rename leaves other connections' owner hints alone
migrateTilesForProfile moves every profile-keyed family to the new name
after a local rename. Tiles, Bot tile owner routes, cached transcript
tails, status drawers and preview dismissals all skip state owned by
another connection, since a same-named profile there was not renamed.
migrateSessionOwnerHintsForProfile had no such check. It rewrote the
hints of remote sessions whose profile had the old name too, and the
hint outranks the session row when routing RPCs. Those sessions then
dialled a profile that does not exist on their connection.

Rewrite only hints on the local connection.
2026-09-28 21:32:35 -05:00
Cad from Arca
e4956f5a12 fix(desktop): surface Tailscale SSH browser checks 2026-09-28 21:32:25 -05:00
Yuan Li
a1725000fc fix(desktop): drained restart polls fail and the restart reconnect probes before closing
Review follow-up on Enough1122's review of #123135:

1. A poll window refused end to end now throws instead of resolving
   success. Only an ANSWERED poll proves the replacement backend came
   back, so the tolerance is reserved for answered polls; a fully
   drained window keeps the callers' failure banners up and toasts.
   The last poll error is retained and surfaced through that path
   (previously every poll error was discarded with no observability).
2. The reconnect handed over after a restart is now a
   'restart-followthrough' source: the boot hook probes liveness first
   and leaves a provably-alive socket alone, defers an inconclusive
   probe mid-turn behind the same bounded re-probe as the wake path
   (#95327), and only closes a provably dead socket. Manual recovery
   keeps its unconditional re-dial contract (it may retry a credential).
3. getActionStatus is no longer passed POLL_TIMEOUT_S as the `lines`
   count; the misleading constant is gone.

Tests now distinguish refused-for-part-of-the-window (success) from
refused-for-the-whole-window (failure) and pin the followthrough
teardown policy on the real boot-hook handler.
2026-09-28 21:29:39 -05:00
Yuan Li
8b24396c01 fix(desktop): reconnect the client socket after a requested gateway restart
Clicking "Restart gateway" restarted the backend cleanly but never touched
the client's own WebSocket: recovery waited on the passive close->backoff
machinery, and on a Windows close-frame-less drop (code 1005) the zombie
socket survived until the 45s heartbeat deadline -- by which time the user
had clicked restart again, re-killing the fresh backend and flapping
forever (#123111). The restart's own action-status poll had the mirror
bug: it polled the very backend the restart takes down, so one refused
request mid-window aborted the wait with a false "restart failed" toast
(and a fresh process's empty registry read as an unknown action).

- awaitAction() now tolerates the restart window: refused polls and a
  fresh process's running:false, exit_code:null are healthy mid-restart
  states; only a recorded non-zero exit (or a drained budget) fails.
- runGatewayRestart()/watchGatewayRestartOutcome() hand reconnection to
  the registered gateway reconnect owner in their finally, collapsing the
  zombie-socket window from the heartbeat deadline to immediate.

Red->green: system-actions.test.ts fails 4/7 on unfixed code, passes 7/7
with the fix; the full src/store suite (1674 tests) stays green.
2026-09-28 21:29:39 -05:00
StanleyStetson
0ffed8e7c4 fix(desktop): retain local secondary profile gateway for foreground sessions
When opening a session owned by a local secondary profile (without an
explicit connectionId), recordSessionEventScope only recorded
sessionOwnerByRuntimeId and omitted sessionScopeByRuntimeId. Furthermore,
foregroundSessionScopes had no fallback to knownOwnerForSession for
active sessions before inbound runtime events had populated the runtime
scope map.

As a result, foregroundPinned evaluated to false once initial request
leases settled (such as session.control.read), causing route.release()
to dispose the secondary gateway socket with code 1005. The frontend
then repeatedly re-attempted read requests, causing an infinite
websocket reconnect loop and high renderer CPU usage (~18%).

Resolve this by:
1. Recording the normalized profile key into sessionScopeByRuntimeId
   when handling events from secondary profile owners.
2. Cleaning up sessionScopeByRuntimeId during forgetProfileOnlyRuntimeOwners.
3. Checking knownOwnerForSession in foregroundSessionScopes for active
   sessions whose runtime scope is not yet registered.
4. Aligning reconcileBusyStatesOnReconnect and liveSessionScopes with
   local secondary scopes, and updating doc blocks.

Fixes #121865
2026-09-28 21:29:30 -05:00
Niklas Kietaibl
75634d5cc2 refactor(desktop): tie mux keepalive to persist timeout 2026-09-28 21:29:20 -05:00
Niklas Kietaibl
64b2ad794d fix(desktop): keep SSH forwards alive across mux idle 2026-09-28 21:29:20 -05:00
liuhao1024
5e1d5f2f4e fix(web): drop the test block accidentally nested inside the stalled-URL test
The gave-up-banner test was pasted twice when the budget-survival commit
(a5e0c9e5c0) was assembled: one copy at describe scope, and a second copy
inside the body of 'times out a stalled URL request and retries'. Vitest
rejects calling it() inside a test ('Calling the test function inside
another test function is not allowed'), which is exactly what failed the
JS & TS checks run on a5e0c9e5c0. Remove the nested copy; the describe-
scope copy and the stalled-URL test are unchanged and pass 20/20.
2026-09-28 21:29:02 -05:00
liuhao1024
121e557ed8 fix(web): make the sidecar redial budget survive client rebuilds (#95951)
The attempt counter lived in the [gw] effect closure, so every redial —
which bumps version and re-runs the effect — reset it to zero and the
five-attempt budget never exhausted. The counter and the gave-up flag
now live in component refs (reset on a successful open and on scope
switches), and exhausting the budget surfaces a one-time banner
('gateway sidecar disconnected — gave up after 5 attempts, use
Reconnect'), mirroring the events feed's give-up contract; the banner
clears on the next successful open.
2026-09-28 21:29:02 -05:00
liuhao1024
d04c351dff fix(web): redial after clean 1012 service-restart closes (#95951)
A dashboard service restart closes both web sockets with 1012 (Service
Restart) — a CLEAN close. The PTY pane's reconnect guard only retried
dirty closes and 1001/1006, so 1012 fell through to the '[session
ended]' path and the pane stranded until a manual relaunch (server logs
in the issue show a 23-minute dead window). The JSON-RPC sidecar had no
redial owner at all: GatewayClient deliberately delegates reconnect
policy upward, but ChatSidebar only dialed once.

- ChatPage: the transient-drop guard now also retries 1012 and 1013 —
  both mean 'the server is coming right back', so the same ?attach=
  token reattaches to the still-living PTY.
- ChatSidebar: the sidecar gets a redial owner with the same bounded
  backoff the PTY pane uses (250ms * 2^n capped at 3s, 5 attempts,
  reset on open, cancelled on unmount/scope switch; the manual
  Reconnect affordance stays the fallback after the cap).

Reported recovery goes from a manual app relaunch (~23 min stranded in
the log sample) to an automatic redial ~6.5s after the service returns.
2026-09-28 21:29:02 -05:00
psam
9d5789e809 fix(desktop): hold the activation lease for the whole switch dial budget (#120298)
The lease that spares a mid-dial switch target from the live-work pruner was a
fixed 30 s, while the dial it must outrun now carries SOURCE_SWITCH_DIAL_TIMEOUT_MS
(135 s, the whole remote bring-up chain). From t=30 s the target was therefore
prunable for the rest of a healthy dial: a recompute disposes the entry, the
bring-up lands anyway, and openSecondary's `!entry.wantOpen` guard closes the
fresh socket and returns — the dial resolving on a closed gateway, with a second
bring-up in flight.

Derive the lease from the dial budget instead of an independent literal so the
two cannot drift again (the argument with-timeout.ts already makes for
BACKEND_BOOT_WAIT_TIMEOUT_MS), and pin it with the convergence case review asked
for: a prune late in a still-in-flight dial must not dispose the target.
2026-09-28 21:28:34 -05:00
psam
5a41f9e8a8 fix(desktop): stop failing a source switch that is still dialing
A remote bring-up is a chain of main-process stages, not one IPC: ssh
connect (15 s), the platform/locate/version execs (20 s each), the spawned
backend's ready sentinel (45 s) and the forward (15 s). A healthy cold
bring-up therefore runs 37-64 s against a small VPS over Windows ssh.

Two renderer awaits bounded that chain with the reconnect-class 20 s budget
(selectConnection's phase-1 dial and openSecondary's registry leg), so the
switch toasted "Could not connect to <source>" while the dial was still
progressing -- and then connected anyway, because withTimeout does not
cancel its input and the dial's socket is retained warm. Users read it as a
broken source and retried.

Bound both dial sites with SOURCE_SWITCH_DIAL_TIMEOUT_MS, the sum of the ssh
layer's own stage budgets, and keep the 20 s reconnect-class budget for the
cheap round-trips (WS re-mint, route probes) and for the local activation
commit it was written for. The #93454 anti-wedge property is preserved: the
per-IPC budgets still fire from inside the dial, and both a wedged dial test
and a slow-but-healthy-dial test pin the behaviour.

Fixes #120296
2026-09-28 21:28:34 -05:00
liuhao1024
7a6407e4ff fix(desktop): auto-recover boundaries from React portal teardown races
Vendor React throws 'Tried to unmount a fiber that is already unmounted'
when a Radix portal (context menu / dialog / popper) unmounts mid-commit
while its host subtree swaps (#98654, also seen in #41693). The throw
unwinds into whatever ContribBoundary hosts the workspace route table, so
the whole pane blanks behind a manual Retry even though the fiber graph
settles as soon as the swap completes.

Extend the existing capped auto-recovery (already used at root for
assistant-ui lookup races) to this error class at every boundary, since
the throw comes from React itself rather than boundary-local app code.
Persistent failures still surface the fallback: 3 recoveries per 5s
window, identical budget semantics.
2026-09-28 21:28:03 -05:00
Brooklyn Nicholson
e85706cba7 fix(tui_gateway): a queued model switch crosses the compute-host boundary
On a compute-host (turn_isolation) session the live agent lives in the child
process, but config.set model either stashed pending_model_switch for a turn
thread that pops-and-drops it against agent=None (busy), or built a second
agent in the serving process and switched that copy while the child kept the
old model (idle). Carry the stash in the turn frame (copied, not popped), let
the child's _ensure_server_session adopt it in both branches, and clear the
server-side stash only after a successful isolated turn.

Rebases tsemana's #79510 onto the post-split modules: the frame key and the
clear-on-success now live in compute_host_bridge.py, adoption in
compute_host.py, and the always-defer condition in methods_config_set.py.

Fixes #79509
Co-authored-by: Tony Semana <tsemana@users.noreply.github.com>
2026-09-28 21:12:23 -05:00
Brooklyn Nicholson
418975fbc5 chore(contributors): map zanzipanzi@gmail.com to @zanzipanzi 2026-09-28 21:11:22 -05:00
Herman Bohdanov
9521d6de55 fix(tui_gateway): hold the /goal continuation after Stop too
_run_post_turn_followups claimed the session for a goal continuation
checking only running, so a Stop that landed after the model answered
but before the follow-ups ran still chained the next goal turn. Refuse
it while the Stop latch is set; the goal resumes after the next prompt.

Bot live delivery stays ungated on purpose: an envelope is a person's
message rather than an automatic turn, and only a local prompt clears
the latch. A comment at the gate says so.
2026-09-28 21:11:22 -05:00