Skip to content

chore(streaming): refresh reasoning-only watchdog on current main - #2

Draft
enzo-adami wants to merge 4 commits into
DavidMetcalfe:fix/78807-reasoning-only-stale-detectorfrom
enzo-adami:agent/pr81526-main-refresh
Draft

chore(streaming): refresh reasoning-only watchdog on current main#2
enzo-adami wants to merge 4 commits into
DavidMetcalfe:fix/78807-reasoning-only-stale-detectorfrom
enzo-adami:agent/pr81526-main-refresh

Conversation

@enzo-adami

@enzo-adami enzo-adami commented Aug 15, 2026

Copy link
Copy Markdown

Purpose

Cross-fork handoff for NousResearch/hermes-agent#81526. This refreshes DavidMetcalfe's three watchdog commits onto upstream main at 165c889e5b4277b56dadd42949a4112c1e6175a6; it is not a replacement PR against NousResearch.

Important

This is a review/branch handoff for a rebased history. GitHub therefore reports it as conflicting against the pre-rebase source branch; it is not intended to be merged mechanically into that old history. The author can inspect it and force-update the source branch to this tested head (or cherry-pick the final oracle commit after performing the same semantic rebase).

Semantic rebase

The only conflict was in agent/chat_completion_helpers.py at the tool-call boundary. The resolution preserves both current-main pending-SSE behavior and the watchdog's progress timestamp, in this order:

_flush_pending_stream_text()
last_content_chunk_time["t"] = time.time()

The three original commits and authorship are preserved. The only additional commit adds two focused regression oracles:

  • pending SSE-looking text is flushed before tool generation begins;
  • tool-call deltas reset the reasoning-only stale window.

Verification

  • 42 passed, 4 skipped across tests/agent/test_reasoning_stale.py and tests/run_agent/test_streaming.py
  • Ruff check: pass on all changed Python files
  • repository Windows-footgun checker: pass on all changed Python files
  • git diff --check: pass
  • no runtime/live installation or configuration changes

The source branch was still exactly ddca016590eeadc94b3d4840e2681a9316fdd33f when this handoff was pushed.

DavidMetcalfe and others added 4 commits August 15, 2026 15:12
…NousResearch#78807)

Models like DeepSeek V4 Flash can emit reasoning_content tokens
indefinitely without ever committing to visible output. Every reasoning
chunk resets last_chunk_time, so the existing stale detector never
fires and the session hangs until the HTTP timeout (up to 1800s).

Add a second timer last_content_chunk_time that resets only on real
output (content text or tool calls), plus a reasoning_seen flag so the
new check only activates once the model has started reasoning (slow
first-token models stay under the existing _stream_stale_timeout
guard). When reasoning-only output exceeds agent.reasoning_only_stale_timeout
(config.yaml, default 300s, 0 disables), the poll loop cancels the
attempt and aborts the request client so the retry loop reconnects.

Config-driven per repo policy (no new HERMES_* env var; the previous
attempt NousResearch#29796 was closed for introducing one). Covers both the
chat-completions (reasoning_content) and Anthropic (thinking_delta)
streaming paths.

Tests drive the real interruptible_streaming_api_call with fake chunk
streams: kill after threshold, config read from config.yaml, disable at
0, no false positive on reasoning-then-content or content-only streams.
Review findings (NousResearch#78807, cross-vendor):

- Anchor last_content_chunk_time at the FIRST reasoning chunk, not the
  attempt start, so TTFT latency does not eat the reasoning budget.
- Track input_json_delta in the Anthropic path: long tool-argument
  streams after reasoning must keep the watchdog satisfied.
- Reject bool config values (bool is an int subclass); 'true' would
  otherwise become a 1-second kill.
- Floor the unconfigured default at the model's reasoning stale floor
  (e.g. deepseek-v4-flash 600s) so the detector never fires before the
  no-chunk detector's established tolerance; explicit config wins.
- Do not retry after a reasoning-only kill: the loop is
  prompt-deterministic, a byte-identical retry would be killed again;
  the outer conversation loop's fallback machinery recovers instead.
Cross-vendor round-2 finding: the configured flag was set for ANY non-None
value, so a malformed value (bool, string) marked the key as configured
and silently bypassed the model's reasoning stale floor. Only mark the
key configured when the value is a valid number; log a warning otherwise
so the floor-aware default still applies.
@DavidMetcalfe
DavidMetcalfe force-pushed the fix/78807-reasoning-only-stale-detector branch from ddca016 to 7271f8d Compare August 16, 2026 23:04
DavidMetcalfe pushed a commit that referenced this pull request Aug 18, 2026
Two independent bugs let a deleted profile reappear / leave orphaned
resources on next launch:

1. hermes_cli/profiles.py's backend-process scanner required argv[0] to
   resolve to an executable literally named "hermes". Electron's
   pool-backend spawn resolves the hermes console-script shim's path and
   execs it via the interpreter directly (python3 /path/to/hermes ...), so
   argv[0] reports as "python3" and the scanner never matched the running
   backend -- delete removed the profile's files but left its live backend
   process running (still bound to a port via uvicorn), which
   accumulates across repeated delete/recreate cycles.
2. The desktop sidebar's ProfileRail only refreshed its cached profile
   list once, on mount, so a delete/create/rename from another surface
   (another window, or the CLI) left a stale ghost entry until something
   unrelated triggered a refetch. Note: a delete via this window's own
   Manage-Profiles view already refreshes the shared $profiles atom
   ProfileRail subscribes to (confirmed by reading refreshProfiles() and
   handleConfirmDelete()) -- this fix only covers the cross-window/cross-
   process staleness gap, not a duplicate of the already-merged
   NousResearch#57329's Manage-Profiles rail-refresh work.

Fix 1: recognize a python-interpreter argv[0] exec'ing a hermes-named
console-script shim via argv[1]. Fix 2: refresh the profile list on window
focus/visibilitychange, matching the existing pattern used elsewhere in
the sidebar (sidebar/index.tsx, use-background-sync.ts, star-map.tsx,
use-gateway-boot.ts all use the same focus+visibilitychange pattern).

## Related work already on main

PR NousResearch#57329 (merged) fixed the *headline* symptom from issue NousResearch#52279
(deleted profile respawns) via a different, non-overlapping mechanism:
routing profile-delete through the primary backend instead of spawning a
fresh pool backend, plus a separate recreation guard in
ensure_hermes_home() (NousResearch#49435, merged) that makes a backend spawned into a
deleted profile's directory raise FileNotFoundError instead of silently
recreating it.

This PR is NOT a duplicate of that fix. Verified: even with both of those
merged, a backend process that survives because of gap #1 above still
holds a bound port via uvicorn -- it just can no longer resurrect the
profile directory. That's real resource-hygiene, not a symptom already
covered. Gap #2 touches a different file/component (ProfileRail /
profile-switcher.tsx) than NousResearch#57329's rail-refresh half (which touched the
Manage-Profiles view's own $profiles.ts / index.tsx) and covers a
distinct staleness path (cross-window/cross-process, not same-window
delete-then-refresh).

Tests: tests/hermes_cli/test_profiles.py -- 156 passed (existing +
regression coverage for the argv[0] python-interpreter detection case).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
DavidMetcalfe added a commit that referenced this pull request Aug 18, 2026
Addresses teknium1's review (NousResearch#64195) finding #2: the multi-rung resolver
needs Electron tests covering precedence, stale-PID rejection, fallback
behavior, and the remote boot path. The pure decision helpers are now
covered by 29 unit tests in `profile-migration.test.ts` (vitest electron
project).

Coverage:
- precedence: legacy > single-running-gateway > state.db heuristic
- stale-PID rejection: recycled PIDs not owned by hermes are dropped
- malformed pid files: JSON parse errors, non-integer PIDs, zero/negative
- scoring edge cases: ancient files (recency floored at 0.1), tiny files
  (size floored at MIN_SIZE), larger DB beats smaller at similar recency
- single-profile fallback: best === 'default' suppresses the write
- no-op cases: preference file already exists, missing profiles root

The remote boot path is verified by code review of the call-site move
(commit preceding this one) — `migrateActiveProfileIfMissing()` now runs
before `primaryProfileKey()` is first read in `startHermes()`.

The pure decision logic that the orchestrator relies on is covered end-
to-end below; this matches the repo's testable-helper pattern (see
`profile-delete-routing.test.ts`).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants