Repository navigation
fix(windows): stabilize Hermes desktop update and startup - #4
Conversation
૮ >ﻌ< ა ci reviewran on de2a123 — Merge remote-tracking branch 'origin/main' into codex/hermes
|
| Package | Before | After |
|---|---|---|
| electron | 40.10.6 |
41.10.4 |
How to fix:
Add the ci-reviewed label after verifying the version changes are expected.
⚠️ Warnings
OSV vulnerability scan · View job
3 known vulnerabilities found in pinned dependencies.
- CVE-2026-67213 in website/package-lock.json
- CVE-2026-71554 in uv.lock
- CVE-2026-71554 in uv.lock
How to fix:
Review the findings in the Security tab. Update the affected dependencies if a patched version is available.
ℹ️ Info
CI-sensitive file review · View job
PR touches sensitive files, but the ci-reviewed label has been added, approving them.
Sensitive files changed:
.github/workflows/ci.yaml.github/workflows/contributor-check.yml.github/workflows/e2e-desktop.yml.github/workflows/history-check.yml.github/workflows/js-tests.yml.github/workflows/nix.yml.github/workflows/review-labels.yml.github/workflows/rust-tests.yml.github/workflows/tests-os.yml.github/workflows/tests.yml
debug info
CI timings
CI timings · View report · View job
Wall time 32m50s vs 1442m29s (-97.7%). 3 job(s) slower, 15 faster, 1 unchanged.
- OS-specific tests / Windows-only tests: -86258.0s
- Rust tests / cargo test (bootstrap installer): -86228.0s
- JS & TS checks / JS & TS checks: -85814.0s
- Python tests / Run tests: -84463.0s
- Lint Docker scripts / Lint docker/ shell scripts (shellcheck): -145.0s
💡 Codex Reviewhermes-agent-almorshednet/hermes_cli/update_inventory.py Lines 263 to 265 in 9f79550 On Windows, if a stale AGENTS.md reference: AGENTS.md:L54-L57 When AGENTS.md reference: apps/desktop/AGENTS.md:L109-L115 For an unknown, removed, or misspelled preset id, AGENTS.md reference: apps/desktop/AGENTS.md:L109-L115 When a preview tab was opened from a relative dotted filename such as AGENTS.md reference: apps/desktop/AGENTS.md:L109-L113 ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
… strand the registry restore Follow-up to the NousResearch#95007 partial cherry: waitForInitialConnection() was an unbounded listen on $connection — a primary that never publishes its descriptor (spawn failure, dead SSH target) would strand initializeConnectionsRegistry() forever and the last-used source would never be restored. Bound it with the codebase's withTimeout helper (same pattern as the sibling SWITCH_* call sites in this file): after 45s (the primary spawn budget) the restore proceeds exactly as it did before the wait existed, and the listener is torn down either way. Regression test: boot restore proceeds after the deadline with the descriptor never arriving. Original-PR: NousResearch#95007
…try one transient projects.tree loss Partial cherry-pick of PR NousResearch#94901 (joe-rodgers). Surviving scope: - api/sessions: stampActiveConnectionOwner — rows returned by the active non-local gateway are stamped with its registry connection_id (explicit owners from multi-source responses preserved), so a later resume cannot fall back to a same-named local profile. - store/projects: one-shot projects.tree retry when a remote source switch leaves the first read RPC on a newly-opened socket without a response (request timed out / gateway connection closed), only while the same gateway/profile is still foreground. Component fix for the live-confirmed NousResearch#92352 sidebar-never-paints gap. Dropped scope (superseded on main / by the NousResearch#94656 anchor landed just below): knownSessionOwner+SessionOwnerScope rewiring in session.ts, session-states.ts, wiring.tsx (main and NousResearch#94656 carry richer variants), and the $connection-derived optimistic-row stamp in use-session-actions/utils.ts (NousResearch#94656 stamps the optimistic row from the captured exact owner route instead of ambient state). Original-PR: NousResearch#94901 Dropped-scope: routing half of 2cb5bdb (session.ts, session-states.ts, wiring.tsx, use-session-actions/utils.ts hunks)
…amping Reconcile NousResearch#94901's API-layer row stamping with NousResearch#94656's durable-owner persistence: extract lib/session-owner-stamp.ts as THE canonical stamp-untagged-rows write path (never clobbers an explicit owner, never stamps `local`) and re-express api/sessions' stampActiveConnectionOwner through it. NousResearch#94656's writers (optimistic row from the captured owner route, mergeSessionPage carry, cache patch) are exact-owner writers and stay as-is; the helper's contract documents why it must not overwrite them. Credit: row-stamping concept from PR NousResearch#94901 (joe-rodgers) and PR NousResearch#95007 (weismanfamily); persistence shape from PR NousResearch#94656 (Zeus-Deus). Co-authored-by: joe-rodgers <25499388+joe-rodgers@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
…ed-in explicit targets A managed cron (created by a provisioning script, not from a live gateway chat) never captures an origin. With cron.mirror_delivery: true and deliver: origin, its brief was delivered to the home channel — the user's own DM — but the transcript mirror and the in_channel session seed were silently skipped: _target_matches_origin returns False for an empty origin, and the whole continuable machinery keys off that check. A user replying to the brief landed in a session with no record of it. Field report 2026-08-17 (enterprise, Slack DM surface). The June origin-scoping refactor (c06ceb3) was written to exclude broadcasts, and the exclusion is kept. What changes is the classification: a home-channel FALLBACK for deliver=origin is the user's primary conversation standing in for the origin, not a broadcast. Changes: - Delivery targets carry a resolution-provenance tag (_resolved_from: origin / origin_fallback / explicit; broadcast expansions untagged). - _target_mirror_eligible replaces the bare origin check at the mirror gate: origin unchanged; origin_fallback eligible under the same flags as origin (per-job attach_to_session wins, else global cron.mirror_delivery); explicit platform:chat targets eligible ONLY under per-job attach_to_session — the global flag never activates them, so it cannot start writing transcript entries into arbitrary explicitly-addressed chats. 'all'/bare-platform stay never-eligible. - Dedup OR-merges provenance so 'origin,all' resolving to the same chat keeps eligibility regardless of token order. - _inchannel_seed_allowed guards the flat-session seed: group-channel session keys are user-isolated, so a seed without a user_id (origin- less job into a shared channel) would create an orphan session no reply resolves to — those targets fall back to the plain mirror. DM targets (keys don't embed user_id) always seed. - cronjob tool schema text updated to describe the new attach scope. Behavioral note: origin-less deliver=origin jobs under global mirror_delivery now activate the full continuable path — on default 'thread' surface this opens a dedicated thread in the home channel where the brief previously posted flat. That is the documented continuable behavior; the silent flat post was the bug. 15 new tests (tests/cron/test_mirror_origin_fallback.py): eligibility matrix (origin/fallback/explicit/all/bare/other-chat), dedup order both ways, end-to-end mirror via _deliver_result for all four shapes, origin regression control, seed user_id guard.
…ance tag tests/cron/test_cron_relay_delivery_guards.py landed on main after NousResearch#89329 branched; its exact-dict assertion needs the new _resolved_from field the salvaged commit adds to origin-resolved targets.
Review finding: the thread-flatten stayed gated on origin_target while the seed gained fallback/explicit eligibility — a threaded origin_fallback or opted-in explicit target would deliver into the thread while the seed created the flat session (the exact split-surface drift the flatten comment warns about). One shared inchannel_continuable gate now drives both, with _inchannel_seed_allowed folded in; is_dm_target hoisted above the flatten and deduplicated.
…licit opt-in) The feature page still said 'only the origin chat is ever touched', which this change makes stale. Documents the three attach-eligible shapes and that the global flag never activates explicit targets (review nit).
- _target_mirror_eligible accepts a precomputed origin_match so the sole production caller stops re-resolving origin + re-running the origin match it computed one line earlier (tests keep the self-contained path). - Document why the fallback branch restates _cron_mirror_delivery_enabled precedence (standalone correctness: per-job False must beat raw global True) instead of collapsing it to the call-site-coupled 'return True'. - Retarget the stale in_channel warn branch from 'not origin_target' to 'not inchannel_continuable' and reword it for the widened seed scope.
The public schema and job store already support per-job attach_to_session, but the registry adapter dropped the argument. Create silently omitted the field; update reported "No updates provided." Fixes NousResearch#84802
…st output Closes the gap flagged in review: the raw store was checked but not the _format_job surface.
…noreply Six commits merged via NousResearch#95366/NousResearch#95367 carry an incorrect author email (kshitijkapoorr@gmail.com — not an address the contributor owns; it was set by tooling error during salvage). The address maps to no GitHub account (verified via the users search API). Canonicalize it to the real identity so shortlog/contributor tooling attributes correctly; the contributors/emails mapping file from the same PRs already covers release attribution.
…p-kshitijk4poor chore: mailmap kshitijkapoorr@gmail.com to kshitijk4poor
…reter Under an SSH remote backend the web server is launched by running the uv BASE interpreter with the venv's site-packages injected into sys.path at startup, so sys.executable is a dependency-less python. Detached dashboard actions spawned from it (Update now, restart, anything routed through _spawn_hermes_action) inherited neither the injected path nor a PYTHONPATH and died on the first third-party import — 'Update now' always failed instantly with ModuleNotFoundError: No module named 'yaml' while 'hermes update' from the venv worked (NousResearch#90026). _dashboard_spawn_executable now prefers the install's own venv interpreter (venv/bin/python, venv/Scripts/python.exe) when it differs from sys.executable, resolving the same dependency set the venv launcher provides. Same-interpreter launches return sys.executable unchanged, preserving the Windows console-ownership behavior verbatim, and layouts without an install venv keep the old fallback.
…path Follow-up on the cherry-picked NousResearch#90030: candidate.resolve() breaks the fix on the standard Linux venv layout, where venv/bin/python is a symlink to the base interpreter. Resolving makes the venv python compare equal to the dependency-less base (so the swap never happens), and returning the resolved target would spawn the bare base interpreter, bypassing pyvenv.cfg — the fix would silently not fix NousResearch#90026 on the exact platform it was reported from. Compare and return normalized UNRESOLVED paths: the venv path IS the interpreter's identity. Adds the symlink-layout regression test; live-E2E'd with a real dependency-less base runtime.
…king the forever chat (NousResearch#92473, part 2) Between the registry miss and the eager session.title write, another writer can take the canonical title (peer dm minting server-side, a second machine, cross-connection sync). UNIQUE(title) rejects our write with 'already in use' — which the compat path previously read as 'old gateway' and prompted into OUR stray lazy session, forking the forever chat. A uniqueness rejection now re-consults the registry and adopts the winner; the zero-message stray is abandoned to the gateway pruner. Genuine old-gateway failures (unknown method) keep the compat kickoff.
…ign anchor copies Integration fixups so NousResearch#82529's generation signing and NousResearch#95131's interpreter anchor cover each other's gaps (without these, each fix leaves the other's rotation path broken): - macos_tcc_anchor store detection now recognizes .hermes-runtime/python/ generation-* stores: repair_vulnerable_runtime() rebuilds the venv against a generation interpreter, replacing the anchored bin/python with a fresh symlink — previously the anchor then read 'not uv-managed' and NEVER re-anchored, so every SQLite CVE repair silently orphaned terminal TCC grants (the exact NousResearch#82427 scenario, path-keyed). - _install_anchor signs the anchor copy with the same identifier-pinned DR (via managed_uv._macos_sign_managed_python) before it goes live: copy2 carries the source build's cdhash-based signature, so an unsigned refresh would still change the stored csreq on every patch bump/repair despite the stable path. Best-effort, never blocks the anchor. - Tests: generation-store recognition + repair-generation anchoring + sign-on-install call (sabotage-verified: dropping the generation root marker fails both new tests).
`/rollback <N>` runs `restore(..., safe=True)` — safe mode is the default,
`--all` opts out. Safe mode splits the changed files into two groups: those
present in the checkpoint are checked out, and those absent from it are treated
as files Hermes created during the turn and deleted, since deleting them is
what restores the pre-turn state.
Absence from the checkpoint is not proof of authorship. `max_file_size_mb`
(default 10) keeps large files out of every checkpoint via
`_drop_oversize_from_index`, so a file the agent appended to — a dataset, a
corpus, an export, a log — is absent for a completely different reason. Safe
mode deleted it. No checkpoint held a copy, so nothing could bring it back, and
`restored_files` listed the path, so the user was told it had been restored.
Reproduced on main with shipped defaults:
corpus.jsonl (2 MB), agent appends to it, then /rollback 1
safe_restore_plan restore=['corpus.jsonl', 'notes.py']
restore ok=True restored_files=['corpus.jsonl', 'notes.py']
notes.py exists=True content="v1 = 'original source'"
corpus.jsonl exists=False <- deleted, was in no checkpoint
Scope: this needs an agent write to the capped file. A large file Hermes never
touched is not in the ledger, lands in `skipped`, and was already safe.
The delete branch now asks whether the path is one the cap would have excluded,
using the same test `_drop_oversize_from_index` applies when building the
checkpoint, so "kept out of the checkpoint" and "refused deletion at restore"
share one definition. Such a path is reported under a new `skipped_oversize`
key and dropped from `restored_files`.
The classification keys on "absent from the checkpoint", not on "large now".
A file small enough to be checkpointed and later bloated past the cap does have
a stored version, and reverting to it is exactly what was asked for — it still
restores, and a test pins that.
The ledger records a content hash, not whether a write created or modified the
file, so an oversize path cannot be proven agent-created. Leaving one behind
costs a stale file the user can delete; removing it costs the file.
Tests: 4 cases in tests/tools/test_checkpoint_manager.py::TestSafeRestore. Two
fail on main — the deletion and the misreport. Two are guards: the
grew-past-the-cap revert, and the small agent-created file that must still be
removed.
Regression: the 10 test files covering checkpoint_manager / rollback —
130 passed on main, 134 with this change (+4 new), zero failures either side.
…ng it Review feedback on NousResearch#95207: `_exceeds_size_cap` and `_drop_oversize_from_index` each computed the byte cap and compared against it. Both used `> cap`, so they agreed, but only by coincidence of two independent expressions — nothing held them together. The coupling is the whole point of the fix. The checkpoint decides what to store and safe restore decides what may be deleted; a threshold that drifted between them would produce a file both absent from the checkpoint and not recognised as capped at restore, which is exactly the deletion this branch exists to prevent. `_drop_oversize_from_index` now calls the predicate. Added a boundary case to TestSafeRestore that pins the round trip from both ends: a file at exactly the cap is stored, so it must revert; one byte more is excluded, so it must be kept. Mutation-checked — moving either side to `>=` fails it, including the re-inlined-with-`>=` shape the reviewer described. No behaviour change: the byte cap, the strict comparison and the unstattable-path result are all as before. Regression: the 10 test files covering checkpoint_manager / rollback, against current main (1fe0f2f, 134 commits newer than the base measured on the first commit) — 130 passed on main, 135 here (+5 new), zero failures either side.
…restart recovery Salvage adjustments to PR NousResearch#94392 per review: - Narrow the supervisor claim to the systemd-VERIFIED path only. The fresh recovery child now probes 'systemctl --user is-active' after each relaunch; only an observed-active systemd unit is reported 'verified'. A relaunch that merely exited 0 is labelled 'relaunch_attempted', never counts as supervisor coverage, and never clears gateway_fleet_restart_incomplete. - Serve-owned runtimes (serve/dashboard entries from the spawn ledger, per the update_inventory serve collector) are no longer silently skipped: the recovery pass records them (and manual gateways) as skipped-with-reason in the recovery result and the persisted update receipt. - Receipt fresh_recovery persists the conservative vocabulary (requested/verified/relaunch_attempted/failed/skipped); 'succeeded' is gone. - Added an end-to-end test that drives the real recovery module in a genuinely fresh interpreter (sitecustomize shim intercepts the grandchild 'gateway restart' and systemctl probes).
…v mutation On Windows installs where the gateway runs as an SCM service (WinSW, NSSM, sc.exe create), the existing pause machinery kills the gateway process directly — and the service wrapper's failure ladder resurrects it within seconds, re-taking the venv file locks mid-update. The update then dies partway through dependency sync with access-denied errors. This extends _pause_windows_gateways_for_update() to detect when a gateway's process tree is owned by a running SCM service, and to stop the SERVICE through sc.exe instead of killing the child: - gateway/status.py: expose service-ownership discovery for gateway runtimes (find_windows_gateway_services maps validated gateway PIDs through process ancestry to running SCM service PIDs, with create-time identity checks against PID reuse). - hermes_cli/update_cmd.py: stop verified services via sc.exe before venv mutation and restart them afterward. Stops wait for a stable SCM 'stopped' state AND for the original descendant processes to exit (service 'Stopped' is not proof the child released its handles). Failure to prove ownership, stop a service, or restart it fails closed; rollback restores attempted services, and rollback failures are surfaced rather than swallowed. - Fail-closed throughout: unreadable identities, ambiguous ancestry, or a service that will not reach a stable state abort the update before any file mutation. Complements NousResearch#37039 (gateway-only concurrent instances no longer abort): that fix lets the update proceed past the gate; this one makes the pause actually stick when the gateway is service-supervised. Note: tests/gateway/test_status.py::TestReadProcessCmdlinePsFallback:: test_ps_fallback_when_proc_unavailable fails on Windows on current main before this change as well (POSIX ps fallback asserted on a platform without it); all other touched suites pass (155 passed, 5 skipped). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…Research#93042 The Desktop can drive a full update of a REMOTE SSH instance: claim the connection (ManagedConnectionUpdateGate pauses dials/mutations while the update owns it), terminate the owned backend with an identity re-proof at the signal boundary (terminateOwnedDashboardForUpdate — argv+creation time re-read in the same remote shell that signals), run the updater under an update-in-progress marker/mutex on the remote install root, bootstrap the new backend, and fence its publication so a rollback cannot leak a half-published serve (fenceManagedSshBootstrapPublication + waitForManagedSshBootstrapFence barrier). Extraction notes (campaign NousResearch#91277 Phase 4; rollout/canary engine stays behind per sequencing): - managed-ssh-update.ts + test: clean cherry-pick (new files). - windows-remote-lifecycle.ts + test: applied as-is (zero main drift). - remote-lifecycle.ts: PR hunks stitched AROUND current main's NousResearch#95532 skew guards and NousResearch#91668 SIGKILL-escalating cleanupStale, which are PRESERVED verbatim — the PR's python identity-re-proof termination is wired for the managed-update path only; connect's stale replacement keeps main's proven kill. One PR test assertion re-pinned accordingly. - One PR test fixed: floating coordinator.start() promise whose rejection IS the contract under test now has an explicit handler (vitest flagged it as an unhandled rejection). tsc clean; 131/131 across the three touched electron suites. main.ts wiring (IPC + deps bag) follows as a separate commit.
Re-implements the NousResearch#93042 main.ts wiring against current main (post-NousResearch#94724 drift), making the extracted managed-ssh-update engine reachable: - ManagedConnectionUpdateGate instance + owner-only recovery journal at DESKTOP_MANAGED_SSH_RECOVERY_PATH (read/write/persist/mark/clear with strict record validation). - IPC: hermes:connections:update-managed (requestManagedSshUpdate with correlation-id claim + in-flight dedupe); update-all's ssh rows now route through the transactional drain/update/restore lifecycle instead of POSTing the remote backend updater. - Gate enforcement at every dial/mutate seam: bootstrapSshConnectionInner (pre-dial + publication fence with exact-serve rollback via rollbackSshBootstrapResult), resolveRemoteBackend, ensureRegistryBackend, saveRegistryConnection dial-field edits, connections:remove, and primary-routing mutations (set-primary, set-launch-mode, connection-config save/apply, profile:set) via assertCanMutateManagedPrimaryRouting. - Scope capture/drain/restore drivers: captureManagedSshScopes (pool + primary discovery, bootstrap fence join), drainManagedSshScope (exact identity-re-proof termination, no-kill forward recovery), ensureManagedSshBackend(AtKey)/restoreManagedPrimarySshBackend restores, openManagedSshUpdateTransport for serve-less connections. - Startup recovery (resumeManagedSshRecoveries before createWindow) and before-quit join of in-flight update/recovery operations BEFORE the SSH coordinator is sealed, so restore dials are not refused during quit. - Extended sshConnections state (spawnNonce/creationTime(Ns)/startedAt/ hermesPath/hermesHome/pythonPath/remoteProfile/registryConnectionId/ primaryRegistryScope) so drain can prove the exact serve it owns; bootstrap coordinator entries carry metadata for the update fence; persistSshConnectionToken mirrors tokens per managedSshTokenPersistencePlan. - preload/global.d.ts: connections.updateManaged + DesktopManagedConnectionUpdateResult/Receipt types. Renderer UI (fleet-updates store, about-settings, system.ts ProfileScope plumbing, i18n) intentionally NOT wired — it belongs to the deferred fleet rollout UI and follows separately. Wiring re-implemented against current main; design from NousResearch#93042 by @andrexibiza tsc -p apps/desktop clean; electron project 1924/1924 passed (133 files, incl. 131/131 across the three engine suites); eslint clean on touched files.
…retry paths (NousResearch#91360) When an update was interrupted or failed mid-install (e.g. dependency install timeout) after pulling new code, the subsequent update run takes the 'commit_count == 0' path and early-returned without checking or migrating the configuration. Fresh code requiring a newer config version would fail to boot on the next run. Extract _check_and_apply_config_migration and invoke it across all update completion paths (normal update, current checkout / node repair, and python dependency repair).
…path (NousResearch#91360) A failed update attempt can pull fresh code onto disk and then die before the config-migration block (e.g. a PyPI timeout during the dependency sync). The desktop hand-off retries; the retry takes the commit_count == 0 branch, repairs deps, prints 'Already up to date!' and returns early - skipping _run_config_check_fresh / migrate_config entirely. The fresh code (requiring a newer _config_version) then refuses to start against the old config until 'hermes doctor --fix' is run. Fix: _maybe_migrate_config_on_current() mirrors the version_bump_only handling (silent, non-interactive) and is called on both repair-path completion points before claiming success. Also: scripts/desktop-update/posix.sh no longer retries when the update was deliberately SKIPPED (checkout parked on a non-target branch) -, the retry is deterministic and only wastes time. Uses a dedicated non- colliding exit code (8) and an honest message instead of 'Update failed'. New tests: tests/hermes_cli/test_update_config_migration_on_current.py (5 cases: migrate-when-behind, noop-current, noop-ahead, warning re- surface, silent check failure).
…reap orphans (NousResearch#61514) Stdio MCP helper subprocesses (npx/binary servers) never import Hermes code, so they could not self-register in the machine spawn ledger and an unclean parent exit left them running invisibly forever. - process_identity.register_child(pid, purpose): ledger mirror of register_self for spawned children — records the CHILD (pid, create_time) with this process as spawner. Refuses pid-only entries a PID reuse could forge. Writes go through the single _append_entry path under _LEDGER_LOCK (prune + atomic tmp/replace unchanged). - 'mcp-helper' added to REAPABLE_PURPOSES so the updater's _ledger_reapable_backend_pids rung flows helpers through its existing spawner_is_dead gate (live spawner => never reaped). - tools/mcp_tool.py: best-effort register_child(pid, 'mcp-helper') at the post-spawn PID capture; never breaks MCP startup. - reap_orphaned_mcp_helpers(): startup sweep mirroring _reap_orphaned_desktop_local_serves but ledger-driven — kills only helpers whose recorded spawner is PROVABLY dead, with a create_time re-check at kill time. Wired next to the desktop serve reap in web_server.py.
The NousResearch#95625 watchdog cancels a step after StepIdleTimeoutSeconds (300s) with no stdout/stderr. But a real `hermes update` is stdout-silent for 40+ minutes by design: the Electron/vite build streams to logs/update.log, not the child's pipes (hermes_cli/update_cmd.py's update-log tee). An output-only ceiling would therefore kill every healthy large update at 5 minutes and mark it exit 124. The drain now fingerprints logs/update.log (size + mtime) and, when the idle ceiling is otherwise reached, treats growth of that file as progress: reset the clock instead of terminating the tree. The stat runs only once the ceiling fires, so the hot drain path never touches the filesystem. HERMES_UPDATE_STEP_IDLE_SECONDS remains the override; HERMES_UPDATE_PROGRESS_LOG points the self-test at its own file. TDD proof: -SelfTestPipeDrain gains a fourth arm, logstall -- a step that is silent on its pipes but appends to the progress log every second and must reach its natural exit 3, never 124. Linux CI pins the same contract at source level (TestIdleWatchdogCountsUpdateLogGrowth); sabotage-verified: making the log-growth consult inert fails test_stall_branch_consults_log_growth_before_terminating.
Fixes NousResearch#93406 (residual). _fleet_probe_expected_runtimes counted the _windows_gateway_resume pause/resume token (profiles/unmapped entries) as an 'expected fleet rows' signal. The token is pause/resume bookkeeping, not a runtime inventory, and its entries have no rows collect_fleet_versions() can return: unmapped Scheduled-Task gateways never publish gateway_state.json, and a resumed profile gateway relaunches detached and may not republish within the probe window. So every Windows update that paused a gateway set _fleet_rows_expected, the verification loop silently waited out its polling window (~14 min wall clock with the retry loop on user reports), printed 'Fleet version check returned no rows', and exited 1 for an update that succeeded. Expected-runtimes now keys only on row-capable signals: restart-phase bookkeeping, the pre-restart PID snapshot, and the pre-update plan inventory -- which already cover any genuinely live pre-update Windows gateway. Counterfactual proof: tests/hermes_cli/test_update_fleet_probe_resume_token.py fails on the pre-fix predicate (token-only => True) and passes with the fix; the row-capable signals are pinned unchanged.
… braces off column 0 The cherry-pick landed the file with CRLF endings and a fixture here-string whose col-0 brace prematurely terminated the handoff test's SelfTest-block strip, tripping the drive-python-not-the-shim guard on fixture code. LF restored (matching main), child-script loop inlined.
.gitattributes forces eol=crlf for *.ps1, so CI checkouts hand the test CRLF content and the SelfTest-strip regex anchors never matched; it went unnoticed while the stripped fixture blocks contained no offenders.
Main's pin (added after this branch was cut) froze the NousResearch#93406 bug as the contract: resume-token services demanded probe rows that SCM-paused services can never produce, stalling every healthy Windows desktop update (NousResearch#95589). The pin now asserts the exclusion; restart-phase and pre-restart-pid signals keep failing closed.
…onnection
Stored session ids are only unique within one profile's state.db, and
localStorage survives profile switches in the same window. The durable
transcript-tail cache keyed entries by bare stored id, so a tail cached
while working in profile A was painted against profile B's backend after
a switch; that backend never held the session and retried it on every
wake ('session not found'), matching the desktop.log pattern of repeated
session.reclaimed (ws_orphan_reap) events for ids that live in another
profile's state.db (NousResearch#94828).
Scope every entry by its owner — the same {connectionId, profile} shape
the REST layer already threads through getLatestSessionMessages and the
in-memory twin stores in TranscriptTailState:
- key entries under v2:[connectionId, profile, storedId]; loads only
return a tail saved under the SAME scope
- thread the session's owning scope through all call sites
(resumeSession's warm-path save + cold-paint load/rollback drop,
final save, removeSession's delete drop)
- sweep pre-scoping v1 entries once per window: a bare-id key cannot be
attributed to an owner, so it must never paint again
- dropTranscriptTail with a scope leaves other backends' same-id tails
intact
…hes on success only Review follow-ups on the salvage (NousResearch#94914): the delete path derives its scope from the removed row while saves derive from ownerRoute — a shape drift orphaned the entry until LRU eviction. Deletes now sweep every scope of the stored id (safe: deleted ids are never reused), while the failed-resume path keeps the exact-scope drop (a same-id twin in another profile must keep its tail). purgeLegacyV1 latches only after a completed sweep so a mid-sweep throw retries next touch.
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Desktop builds legitimately produce no output for minutes on Windows (the updater terminal is static until a step completes), so 300s risks cancelling healthy long steps. 600s keeps the watchdog meaningful for true stalls while clearing slow builds; HERMES_UPDATE_STEP_IDLE_SECONDS still overrides. Per Teknium's review.
493df020 supersedes a91a4cc2 as the 0.3.0 pin target: same /new reset funnel, plus the profile-home resolution fix for issue #4 (readers under a per-profile HERMES_HOME double-nested profiles/<name> and silently found no policy). Verification (repro at the old pin, plugin doctor, 32-test suite) is in the PR body and on the issue.
Pin to bc19ef8, which includes the Reset scoping fix (#4) and the Monitoring Dashboard display-name rename. Keeps the stable install id.
Scope
Verification
Guardrails