Commit Graph

22360 Commits

Author SHA1 Message Date
kshitij
0b36a5be42 bye bye 2026-08-14 13:55:29 -07:00
kshitij
924074906e perf: precompute intervals outside lock, cache croniter, use _ensure_croniter
/simplify-code findings:
- Precompute job intervals OUTSIDE _running_lock in sweep_stale_inflight so
  croniter evaluation does not block try_register/release_running_job (efficiency HIGH).
- Add _cron_interval_cache so cron expression cadence is computed once, not
  every 60s tick (efficiency MEDIUM).
- Use cron.jobs._ensure_croniter() instead of bare 'from croniter import
  croniter' — reuses the existing lazy-import infrastructure (reuse HIGH).

Noted as follow-up (too invasive for salvage):
- Consolidate _cron_interval_minutes + _job_interval_minutes with existing
  _compute_grace_seconds in cron/jobs.py (reuse HIGH, cross-module refactor).
- Extract shared _append_jsonl helper from _record_forced_release +
  _write_usage_audit (reuse HIGH, touches existing code).
- Collapse _running_job_ids + _running_since + _running_futures into a single
  dict of records (quality HIGH, redesigns contributor's core structure).
2026-08-15 02:23:56 +05:30
kshitij
24a9203008 chore: map contributor email devops@sycamore.group → sycamoregroupltd 2026-08-15 02:23:56 +05:30
kshitij
50febe97d9 fix: follow-up for salvaged PR #86129 — config.yaml, eliminate redundant load_jobs, consolidate dispatch
- Read inflight_max_minutes from config.yaml first (cron.inflight_max_minutes),
  keep HERMES_CRON_INFLIGHT_MAX_MINUTES env var as internal escape hatch only.
- Skip the redundant load_jobs() call in tick() when there are no in-flight
  claims or when due_jobs already covers the in-flight set (get_due_jobs calls
  load_jobs internally, so this avoids a second file read on every active tick).
- Consolidate _job_interval_minutes by normalizing string schedule to dict
  first, eliminating ~8 lines of duplicated dispatch logic.
2026-08-15 02:23:56 +05:30
devops
e14248ac1e fix(cron): self-heal leaked in-flight claim so a wedged recurring job re-dispatches (t_8b5480b3)
Port t_3778a491's in-flight stale-claim guard, absent from origin/main.

_submit_with_guard adds a job id to _running_job_ids before the future
that owns its release exists. Anything that hangs or dies between the
add and pool.submit (EAGAIN thread exhaustion on a substrate spike, or a
wedged SessionDB.__init__ on a stale sqlite flock) leaks the claim; every
later tick short-circuits with 'already running - skipping' silently - no
execution row, no last_error, no counter - until the gateway process
restarts. This wedged 4 recurring no_agent router/watchdog jobs (verdict-
router, wake-scanner, auto-review-router, blocked-task-notifier) for ~1h47m
on 2026-08-14 (t_20e23f84), cleared only by manual force-run.

- Record claim timestamp + pending-future sentinel in the same critical
  section as the add; replace sentinel with the owning future after submit.
- sweep_stale_inflight() runs every tick (even idle) and force-releases
  claims older than max(2*interval, 30m floor) with no live future: WARNING
  cron.inflight.forced_release, get_inflight_guard_stats() counter, JSONL
  record, and mark_job_run(success=False) so the wedge surfaces as last_error.
- Wrap the pre-future init (create_execution/copy_context) so an exception
  there releases the claim immediately instead of leaking it.
- Finite-repeat jobs are released without mark_job_run so a forced release
  never consumes a one-shot budget.

Scheduler-internal only: no provider/model routing, no credentials, no
spend, no guardrail weakening, no cron permission widening.

Tests: tests/cron/test_inflight_stale_guard.py (18), plus regression tests
for the recurring EAGAIN re-dispatch and the create_execution/pool-submit
leak paths. Full tests/cron/: 616 passed.
2026-08-15 02:23:56 +05:30
kshitij
367f0c21ed feat(agent): resolve sequential tool deadline via timeouts.tools.sequential_call (#85125 2a)
Follow-up on the #84795 salvage: the sequential deadline gets its own
resolver key. Unset, it inherits the concurrent batch deadline (same
value, same HERMES_CONCURRENT_TOOL_TIMEOUT_S bridge) so the two executor
paths cannot drift by default; set, it can be tuned or disabled
independently. Documented in cli-config.yaml.example; 5 contract tests.

Deliberately NOT on run_bounded_sync: the executors extend deadlines
dynamically during human approval waits (authorization-gate excluded
seconds) — the shared primitive is fixed-deadline. Noted in the docstring.
2026-08-15 02:21:39 +05:30
fangliquanflq
61645cde82 fix(agent): exempt clarify from sequential tool deadline
Clarify waits on a human for up to 3600s or unlimited. The generic sequential timeout was aborting that wait at 420s and leaving the prompt and worker active.
2026-08-15 02:21:39 +05:30
fangliquan
82a1b5a115 fix(agent): suppress late timeout observer events 2026-08-15 02:21:39 +05:30
fangliquanflq
ededa8c4f1 fix(agent): bound sequential tool calls 2026-08-15 02:21:39 +05:30
Teknium
4fe5090964 chore: contributor email mappings for salvaged PRs 2026-08-14 13:51:26 -07:00
algf
2912093e06 fix(dashboard): redraw TUI after PTY reattach 2026-08-14 13:51:26 -07:00
LeonSGP43
6356dc392e fix(ui-tui): redraw after session resume 2026-08-14 13:51:26 -07:00
John Lussier
96e794aa4a fix(tui): redraw after terminal focus regain 2026-08-14 13:51:26 -07:00
Teknium
20e5d51bea fix(browser): managed-first browser-use CLI resolution
Everything Browser Use is now managed by Hermes: the canonical binary
is the one install_cli() provisions into HERMES_HOME/bin, and every
resolution and provisioning site prefers it.

- _find_cli(): probe order flipped to managed bin -> PATH ->
  user-level tool dir (then uvx across the same order). A user's own
  uv tool install can no longer shadow the Hermes-managed copy with a
  drifted version; side installs only matter when we have nothing.
- install_cli(): a browser-use on PATH no longer short-circuits the
  install — only the managed copy does, so selecting any backend
  provisions the copy Hermes controls and updates.
- _ensure_browser_use_cli() (hermes tools): drops its own PATH check
  and always delegates to install_cli(), the single owner of the
  managed-copy policy.
- install.sh / install.ps1: same short-circuit fix — only
  HERMES_HOME/bin/browser-use counts as installed.

Follow-up to #86240 and #86320: with every non-Camofox backend
selection installing the CLI, managed-first closes the remaining
version-drift/shadowing class instead of guarding single sites.

Tests updated to pin the new contract: managed beats PATH and
user-local; PATH install does not satisfy install_cli; helper always
delegates. E2E-verified precedence chain with real files and a real
degraded-PATH install attempt.
2026-08-14 13:51:07 -07:00
Brooklyn Nicholson
6f49bc7d52 chore: map contributor email for the salvaged Windows updater commit 2026-08-14 15:48:31 -05:00
Brooklyn Nicholson
f696380d07 test(desktop-update): guard the Windows hand-off python.exe contract
Source-level regression for the Windows Desktop update self-lock: assert every
Invoke-HermesStep call in scripts/desktop-update/windows.ps1 drives $pythonExe
via `python.exe -m hermes_cli.main`, never the hermes.exe shim. Driving the
update through the shim keeps hermes.exe mapped as a running image, so uv's
final `pip install -e .` shim rewrite fails with os error 32 and the update can
never complete. Runs on Linux CI (no PowerShell execution needed).

Co-authored-by: Sascha Haase <sascha.haase@textiletsg.com>
Co-authored-by: adamcap926 <adamcap926@users.noreply.github.com>
2026-08-14 15:48:31 -05:00
Sascha
5bfb7ee42f fix(desktop-update): drive the Windows hand-off through the venv python, not the hermes.exe shim
`uv pip install -e .` has to replace the console-script shims, so
_quarantine_running_hermes_exe must first rename the running hermes.exe out
of the way. That rename fails whenever any child process spawned from that
hermes.exe is still alive: on Windows a child inherits a handle on the parent
image. It is the inherited handle, not the trampoline, that pins the file --
killing the child makes the identical rename succeed, and the shim flavour
(uv trampoline vs distlib launcher) makes no difference.

The updater spawns such children itself (npx cache warm, memory-provider
refresh -- hindsight-api runs as a daemon with --idle-timeout 300 and outlives
the step that started it), so this presents as a race rather than a hard
failure: the same hand-off succeeds on one run and dies on the next. Step 2's
shim-unlock preflight cannot catch it, because the shim genuinely is unlocked
at that moment; the pinning child appears later, during the update.

When the rename loses that race, _schedule_replace_on_reboot is the last
resort -- and MOVEFILE_DELAY_UNTIL_REBOOT writes to HKLM, so it needs
elevation. A Desktop-driven update is not elevated, so it returns
ERROR_ACCESS_DENIED, `uv pip install -e .` exits 2, and the ZIP fallback
repeats the identical sequence. The desktop build stage is then never reached
while the pre-build clean has already removed apps/desktop/release, leaving an
install whose Start Menu shortcut points at a Hermes.exe that no longer exists.

Running the same code as `python.exe -m hermes_cli.main update` puts the
inherited handles on python.exe, which uv never has to replace.

posix.sh is deliberately untouched: unlinking a running executable is legal
there, so the equivalent call is harmless.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 15:48:31 -05:00
Teknium
3f39f80355 fix(update-check): never flag local-ahead checkouts as updates (desktop SSH sibling)
Same local-ahead blind spot as the CLI SSH fast path, on the desktop's
passive SSH-official check: tips differ but ahead_by == 0 means the remote
tip is reachable from HEAD (carried local commit). Treat that as up to date
instead of 'update available' — the nudge toward hermes update is exactly
what wipes carried work.

Widens andyst-dev's #84860 to the desktop sibling site.
2026-08-14 13:46:33 -07:00
andyst-dev
898d786ad7 fix(update): count real behind commits in SSH fast path
Fixes #84851

The SSH fast path in _check_via_local_git compared only the exact tip SHA
of local HEAD against upstream main. When a local carried commit makes the
SHAs differ, it returned 1 ('behind') without checking ancestry, so an
ahead-of-origin checkout was misreported as '1 commit behind' — nudging the
user to run 'hermes update' and wipe their carried work. Fall back to
git rev-list --count HEAD..origin/main (mirroring the full-clone path) when
the SHAs differ.
2026-08-14 13:46:33 -07:00
Teknium
29760fe6b1 perf(desktop): reuse the chip preview bytes at image submit
The composer read the same image off disk twice: once in attachImagePath
for the chip thumbnail (previewUrl — the FULL file as a base64 data URL),
and again inside uploadComposerAttachment at submit for the upload bytes.

readImageForRemoteAttach now accepts the attachment's previewUrl and
reuses its bytes when it is a base64 data URL, skipping the second disk
read + IPC round-trip. Anything else (e.g. a gateway media URL) falls
through to the disk read unchanged.

Flagged in #86302's renderer bench as the one real renderer-side
inefficiency on the attach path.
2026-08-14 13:44:02 -07:00
AlexMnrs
ca1b3f8705 fix(windows): avoid locale-sensitive update timestamps 2026-08-14 15:41:38 -05:00
Brooklyn Nicholson
cd104ed49e fix(desktop): let you type spaces and arrows in the session rename dialog
Renaming a session from its row menu opened the rename dialog, but the
menu's close restored focus to its trigger — the session row's own
<button> — instead of leaving it in the dialog input. Focus sat on the
row, so Space toggled the row (selecting/deselecting the session) and the
arrow keys moved the list rather than the text caret; you couldn't type a
space or move within the name.

Thread onCloseAutoFocus through the shared ActionsMenu / ActionsContextMenu
primitives and suppress that one focus-restore when the rename item is the
action that closed the menu, so the dialog input keeps focus. Every other
action leaves the restore untouched. Mirrors the project menu's existing
appearance-popover guard.
2026-08-14 15:40:23 -05:00
teknium1
f79440e0f4 feat: /loop — recurring in-session wakeups (Claude Code parity)
Ports Claude Code's /loop (and its /proactive alias) across every Hermes
surface. /loop [interval] <prompt> re-runs a prompt or slash command on a
recurring cadence inside the live session; omitting the interval enables
self-paced mode (starts at the floor, backs off exponentially while the
agent's replies stop changing, snaps back on change — local digest
comparison, zero extra LLM cost).

Stop conditions: agent-emitted LOOP_COMPLETE marker, --times N,
--until <condition> (judged by the existing goal_judge aux task,
fail-open), /loop stop, and a loops.max_ticks backstop budget.

Core: hermes_cli/loops.py (LoopState + LoopManager + shared
dispatch_loop_command), persisted per session in SessionDB state_meta
(loop:<sid>) so /resume picks it up; migrates across compression
boundaries like /goal. New SessionDB.list_meta_prefix() powers the
gateway's cross-session scan.

Surfaces:
- CLI: /loop handler + idle-fire and post-turn-complete hooks in
  process_loop (mirrors the /goal hook shape; Ctrl+C pauses the loop)
- Gateway: /loop handler with route capture, mid-run control-verb guard,
  post-turn tick completion, and a supervised loop_wakeup_watcher that
  injects due wakeups into idle chats via the synthetic-message path
- TUI/dashboard/desktop: command.dispatch handler + per-session
  notification-poller wakeup driver + post-turn completion in the turn
  dispatcher; /loop added to the desktop slash palette
- /goal mixing: an active non-parked goal owns the idle boundary — loop
  ticks defer until it finishes, pauses, or parks; real user input always
  wins over both

Config: loops.{min_interval_seconds,max_ticks,self_paced_floor_seconds,
self_paced_ceiling_seconds}. Docs page + sidebar entry. 77 new tests.
Slack's 50-slash cap: /version moves to /hermes version to free the
native slot for /loop.
2026-08-14 13:40:19 -07:00
Brooklyn Nicholson
c7a243d785 feat(desktop): tag each sidebar project row with data-sessions-project
The merged data-attributes only exposed data-sessions-project on the
sessions wrapper once a project was entered, so in the project overview
(and every other mode) the attribute was absent. Put it on each project
overview row too, carrying that row's project id, so a custom skin can
target an individual project from the list — the parallel to the entered
wrapper's attribute.
2026-08-14 15:39:08 -05:00
Teknium
d8d7cc068d fix(update): stop reporting bogus 'Found 9980 new commit(s)' on shallow installs
The hermes update APPLY path still ran an unconditional
rev-list --count HEAD..origin/<branch> — on a depth-1 installer checkout
that walks the truncated graph and reports the entire remote ancestry
(#53479's 'Found 9980 new commit(s)' on Windows 11). The zero/nonzero gate
stays (a 0 count is trustworthy on any graph); when the count is positive
on a shallow repo, recover the real number via the GitHub compare API
(added in PR #86257) and print count-free wording when that fails.
ahead_by==0 (local-ahead) falls through to the up-to-date path.

Completes the class fix from PR #86257 on its last remaining site.
2026-08-14 13:36:42 -07:00
kimyxx
d29abb7e6b fix(browser): discover browser-use from user-level tool directories
Desktop/TUI workers can spawn with a minimal PATH that omits
~/.local/bin, the default location where uv tool install links the
browser-use binary. _find_cli() then failed to resolve an installed
CLI and Browser Use mode silently fell back to the built-in tools.

Probe the user-level tool dir (~/.local/bin on POSIX, APPDATA/uv/bin
on Windows) between PATH and the managed HERMES_HOME/bin, for both
the browser-use binary and the uvx fallback.

Salvaged from PR #83788 by @kimyxx onto current main; tests adapted
and extended with precedence and uvx coverage.
2026-08-14 13:32:03 -07:00
Brooklyn Nicholson
c59e30f14c perf: cover all six attach RPCs and report surface exposure
The bench timed two of the six RPCs the fix touches. Extend it to
image.attach, pdf.attach, clipboard.paste and image.detach so every changed
handler carries a number rather than an inference.

Also report which surfaces reach these RPCs at all, since "why was the GUI
special" is the first question the fix invites. CLI attaches inline in its
own turn path with the agent already built, so it cannot reach the stall;
the TUI calls the same RPCs and was equally exposed. The difference was hit
rate, not code path.
2026-08-14 13:24:40 -07:00
Brooklyn Nicholson
9d4ba40672 perf: benches for the image attach path
gateway_attach_bench.py drives the real dispatcher with a session whose agent
build is still running and times each attach RPC against prompt.submit as the
control — the harness that located the stall and measures it.

image-attach-bench.mjs times the renderer-side transforms (file read, base64,
RPC frame, embedded-image extraction, render-weight walk) across image sizes.
It is what ruled the renderer out: ~26ms total at 3MB.
2026-08-14 13:24:40 -07:00
Brooklyn Nicholson
7de5634a2f test: attach RPCs complete while the agent is still building
Behavior contracts, not timings: each handler must return with the session's
agent_ready event still unset, the staged image must still reach the turn,
and an unknown session must still be rejected. Verified to fail against the
unfixed resolver (4 failed, 90s of real stalls) rather than only passing
against the fix.
2026-08-14 13:24:40 -07:00
Brooklyn Nicholson
8b06f7df8e fix: attach RPCs no longer wait on the deferred agent build
image.attach, image.attach_bytes, file.attach, pdf.attach, clipboard.paste
and image.detach resolved their session through _sess(), which blocks on
_wait_agent(). None of them needs the agent — they read cwd/profile_home and
mutate attached_images, all populated when the session record is created.

None of these methods is in _LONG_HANDLERS either, so the wait ran inline on
the socket reader thread. Attach runs before prompt.submit, so pasting an
image into a session whose deferred build was still warming (MCP discovery,
model metadata, skills scan) stalled the send and every RPC queued behind it
on the same socket, with no spinner to explain it. prompt.submit already
resolves via _sess_nowait and waits later, off the reader thread — which is
why the symptom reads as "text is instant, images hang".

_sess_building() resolves the session and still kicks off the build (so the
following prompt.submit finds a warm agent), it just doesn't block on it.
_sess() is now expressed in terms of it, so the two differ in exactly one
way: the wait.
2026-08-14 13:24:40 -07:00
Teknium
bf10349ebd chore: lint fixes (blank line, useMemo dep) 2026-08-14 13:09:44 -07:00
Teknium
cc9358e525 chore: map contributor emails for salvaged commits 2026-08-14 13:09:44 -07:00
Teknium
9442a718da fix(update-check): recover the real behind-count via the GitHub compare API
The honesty half (no fabricated counts) leaves shallow installs permanently
count-less. The compare API knows the full graph regardless of local clone
depth: GET /repos/<o>/<r>/compare/<current>...<target> returns ahead_by —
exactly the behind count the shallow boundary lost.

- hermes_cli/banner.py: _github_compare_behind() (bounded, unauthenticated,
  best-effort); wired into _check_via_rev and the shallow branch of
  _check_via_local_git. ahead_by==0 with differing tips = local-ahead => 0.
- hermes_cli/update_cmd.py: hermes update --check shallow path prints the
  exact count when recoverable, presence-only wording otherwise.
- apps/desktop/electron/update-count.ts: compareApiUrl() +
  parseCompareBehindCount() pure helpers; main.ts fetches the count when
  resolveBehindCount() returns null, and the SSH-official passive path stops
  fabricating behind:1 (uses compare API + updateAvailable flag).
- apps/desktop/src/lib/version-status.ts: updateAvailable now applies to the
  client target too, so a shallow desktop install shows '(update)' instead of
  nothing (or the old frozen '(+1)').

Fixes #84591; CLI siblings of #78253 / #53479 behavior.

E2E: live compare API returned 61/62 for real 61/62-commit gaps and 0 for the
reversed (local-ahead) pair; real shallow-clone fixture (depth-1 clone +
depth-1 fetch, merge-base broken) recovers the exact count with the API and
falls back to the honest sentinel offline.
2026-08-14 13:09:44 -07:00
metamindedu
3dbdea8b8b fix(desktop): make shallow update status presence-only 2026-08-14 13:09:44 -07:00
Dolverin
54bd47b811 fix(desktop): show 'update available' instead of fake '1 change included' on shallow clones
On an installer checkout (clone --depth 1) with no merge-base against the
freshly fetched origin tip, resolveBehindCount returned the sentinel 1 and
every surface rendered it as a literal count: 'A new update is ready (1
change included).' — even when the true distance was far larger (observed:
90 commits). The sentinel was meant to mean 'update available, exact count
unknown', but nothing downstream distinguished it from a real one.

- update-count.ts: return null (unknown) instead of the numeric sentinel
- main.ts: flag updateAvailable explicitly and still serve the (capped)
  commit log so 'See what's new' stays useful in the unknown case
- updates.ts: toast fires for behind:null + updateAvailable, with
  count-free copy instead of being swallowed by the <= 0 guard
- about-settings.tsx: status line and action buttons key off
  updateAvailable; unknown size renders the new count-free string
- i18n: updateReadyUnknown / updateReadyMessageUnknown in all 5 locales

Refs #51922 (the shallow-clone special case this UI now renders honestly).

Tests: vitest electron 10/10, ui 44/44 (3 FAIL-BEFORE reds turned green),
tsc typecheck clean, eslint clean on all touched files.
2026-08-14 13:09:44 -07:00
gkd2323c
294071bf08 fix(banner): stop fabricating '1 commit behind' on SSH-official remotes
The SSH-official-remote path in _check_via_local_git was hard-coded to
return 1 when _check_via_rev reported UPDATE_AVAILABLE_NO_COUNT, so
'hermes --version' and the CLI banner surfaced a stable but false
'Update available: 1 commit behind — run hermes update' message. The
count never grew: whether upstream was 1 commit or 100 commits ahead,
the banner always said '1 commit behind'.

Root cause: an ls-remote probe against the upstream URL can only tell
us tip SHAs, not a real commit count. Returning the sentinel
UPDATE_AVAILABLE_NO_COUNT (-1) already means 'update exists, count
unknown' — the exact right shape for this path.

The dashboard/desktop UI does not depend on the fabricated 1:

- The REST /api/hermes/update/check endpoint
  (hermes_cli/web_server.py::check_hermes_update) treats any nonzero
  behind as update_available=true, and its docstring explicitly
  documents -1 as a legitimate value.
- The desktop store (apps/desktop/src/store/updates.ts::mapBackendCheck)
  clamps behind<=0 to 0 and reads updateAvailable as a separate boolean
  field.

So restoring the sentinel is a strict improvement: CLI banner and
hermes --version now say 'Update available' honestly instead of
inventing a count, and every REST/desktop consumer keeps working.

Changes:
- hermes_cli/banner.py: drop the 'return 1' override in the SSH branch;
  propagate the sentinel unchanged.
- hermes_cli/main.py::_print_version_info: render the sentinel as
  'Update available — run <cmd>' (without a count).
- tests/hermes_cli/test_update_check.py: update the SSH-official test
  to assert on the sentinel; add 3 new tests covering the CLI
  renderer's -1 / >0 / 0 branches.
2026-08-14 13:09:44 -07:00
Teknium
c2bf1ac70a chore: contributor email mappings for salvaged PRs 2026-08-14 13:09:37 -07:00
Teknium
3bd98ec4bf fix(cli): redraw on terminal focus regain + docs for scrollback rebuild config
Completes the duplicated-chrome class fix:
- Focus-in (CSI I) now routes through the same rate-limited full-redraw
  recovery as Ctrl+L//redraw, clearing ghost prompt/composer copies after
  Alt+Tab / tab switches (focus-regain variant reported on #60920, #25337)
- Document display.cli_rebuild_scrollback_on_redraw in configuration.md
- Register the new default in hermes_cli/config_defaults.py (moved from
  the pre-refactor config.py location the salvaged commit targeted)
2026-08-14 13:09:37 -07:00
HunterSThompson
c3d7f6eefd fix(cli): recover prompt_toolkit paint after tmux attach
Same-width SIGWINCH (typical tmux attach) skipped screen clear and
left previous_screen inconsistent, crashing redraw with
'cell' object has no attribute 'char'. Always clear on resize
recovery and retry _output_screen_diff with previous_screen=None
on AttributeError/TypeError.
2026-08-14 13:09:37 -07:00
angeon
88a1a9fd95 fix(cli): let redraw recovery rebuild scrollback 2026-08-14 13:09:37 -07:00
angeon
0e1cba326b fix(cli): honor persisted status bar visibility 2026-08-14 13:09:37 -07:00
halaprix
a22fbba340 fix(cli): don't replay transcript on the session's first benign SIGWINCH
The resize recovery treated the first SIGWINCH of a session as a width
change (no prior width to compare against), running the Ctrl+L-style
viewport clear + _OUTPUT_HISTORY replay. The 2J clear preserves
scrollback, so everything in the deque printed a second copy below the
still-visible original. After --continue/--resume the deque holds the
whole "Previous Conversation" recap plus the first live exchange, so a
benign resize signal (GNOME Terminal tab bar appearing, monitor-scale
change, focus events) duplicated the entire conversation.

Seed the width baseline when the resize hook is installed, and replay
only on an observed width change. The baseline is read from app.output
— get_app() at install time is still the DummyApplication whose
DummyOutput reports a fake 80 columns, which would turn the first real
signal back into a phantom width change. A real initial maximize or
restore still differs from the seeded width and is still recovered
(#49120 behavior preserved; verified in a pty harness both ways).

Fixes #65293
2026-08-14 13:09:37 -07:00
Alli
6625c72c92 fix(cli): use _suspend_output_history for interrupt marker instead of clearing _OUTPUT_HISTORY
The original fix for #60920 cleared _OUTPUT_HISTORY in _recover_terminal_after_interrupt
to prevent the interrupt marker from being replayed on redraw. This approach:
- Discarded legitimate scrollback content unnecessarily
- Broke /redraw and Ctrl+L replay for any content after an interrupt

Instead, the interrupt marker is now printed via _cprint inside a
_suspend_output_history() context so it never enters _OUTPUT_HISTORY.
_recover_terminal_after_interrupt no longer needs to clear history — the
marker was never recorded, so _replay_output_history cannot duplicate it.

Also adds:
- _show_interrupt_marker flag to cleanly separate marker rendering from
  response construction
- Focused regression tests covering the marker recording suppression,
  history preservation after recovery, replay cleanliness, and flag logic

Fixes: #60941
2026-08-14 13:09:37 -07:00
Teknium
3885c1096a fix(gateway): stop internal bookkeeping writes from advancing the session activity clock
set_session_metadata() and advance_compression_session()'s repoint both
stamped entry.updated_at = now. updated_at is the user-activity clock that
drives idle/daily reset policy and the restart-resume freshness gate
(suspend_recently_active, #85709), so a background metadata write (e.g.
Slack thread watermark) or a background compression repoint on a long-idle
session could make it look freshly active and get it falsely resume_pending
after a gateway restart.

These are the last internal stamp sites after 784f733cf (recover) and
5462f689b (touch_activity gating): drop the stamps, keep the durable save.

Follow-up to #85895 (closed) — credit @GodsBoy for the report-side push and
@chelsealong for the analysis on #85709.
2026-08-14 13:08:39 -07:00
Teknium
1169fb50a4 fix: install Browser Use CLI for every browser backend except Camofox
The Browser Use CLI 3.0 is the primary driver engine for all browser
backends except Camofox, but only the explicit 'Browser Use' picker row
ran the install hook. Local Browser, Browserbase, Firecrawl, and the
Nous-managed cloud rows left the CLI uninstalled, so those selections
depended on the uvx zero-install fallback (first-use PyPI download
inside the tool-call timeout) or silently downgraded to the built-in
browser tools where uvx was unavailable.

- Extract the install logic into _ensure_browser_use_cli() and run it
  from the agent_browser/browserbase post_setup branch too (Firecrawl
  and the Nous cloud row both declare post_setup: browserbase).
- Camofox is untouched: Firefox-based, no CDP surface, cannot be driven
  by the CDP-only browser-use harness.
- Failure stays non-fatal: uvx fallback, then built-in tools.

Tests pin the contract: every browser post_setup key except camofox
attempts the install; camofox never does; install failure never raises.
2026-08-14 13:05:14 -07:00
kshitij
d6a5cb9725 Merge pull request #85147 from kshitijk4poor/feat/unified-deadline-layer
feat(agent): unified deadline layer — bounded execution primitive + timeout resolver (#85125 Phase 1)
2026-08-15 01:10:40 +05:30
Teknium
a90d5369f7 feat(gateway): dump wedged worker stacks when the turn reaper fires
When the inactivity reaper interrupts a timed-out turn, the interrupt
frees the blocked frame — destroying the only evidence of where the
turn was wedged. The Aug 2026 zombie-turn incident (WhatsApp session,
Relay-corrupted scope stack) wedged every turn for exactly the 1800s
timeout somewhere between 'Turn ended' and run_sync returning, and the
wedge point was unprovable post-mortem.

The reaper now logs the stack of every thread with turn-machinery
frames BEFORE interrupting, so the next occurrence names the exact
blocked line. Best-effort, bounded (8 threads, 25 frames), pure
in-process, never raises into the reaper.
2026-08-14 11:22:14 -07:00
Teknium
afe09c7942 fix(gateway): exclude wedged turns from the restart after-turn wait
A turn idle past agent.gateway_timeout (the same threshold the turn
reaper uses) no longer defers an in-band restart. Restart is usually
the remedy for a wedged turn; waiting restart_after_turn_timeout on
one inverts the graceful path's purpose — a wedged WhatsApp turn
pinned 'hermes update' in draining until SIGTERM was sent manually
(Aug 2026). stop()'s bounded drain interrupts wedged turns instead.

gateway_timeout=0 (unbounded turns) disables wedge detection; cron
and API-server work has no per-turn activity clock and is never
counted as wedged; unreadable activity summaries fail open.
2026-08-14 11:22:05 -07:00
Soheil Fakour
0c6761c511 fix(gateway): restart_after_turn_timeout default 6h -> 30min (#79133)
The 21600s (6h) default shipped in #77184 makes an interactive
'hermes gateway restart' block for up to six hours when a turn wedges
(hung tool call, wedged event loop, stuck provider stream) — the exact
scenario the cap exists for. The intent (don't force-kill an agent
mid-turn) is sound, but the default must be a safety valve for hung
agents, not a target latency.

Lower to 1800s (30 min): still protects the overwhelming majority of
long autonomous turns (tool calls have their own timeouts well below
that), keeps worst-case interactive restart latency human-tolerable, and
users running very long unattended turns can raise it in config.yaml.

RED: new contract test fails on old 21600 default. GREEN: 4/4.
2026-08-14 11:22:05 -07:00
rob-maron
0e4e8baf63 add deepseek v5 pro 0813 to model catalog (#86256) 2026-08-14 18:14:27 +00:00