hermes - ✅(Solved) Fix feat: Skip npm install during updates when Node dependencies haven't changed [1 pull requests, 1 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
NousResearch/hermes-agent#17268Fetched 2026-04-30 06:48:43
View on GitHub
Comments
0
Participants
1
Timeline
4
Reactions
0
Participants
Timeline (top)
labeled ×3cross-referenced ×1

Root Cause

Impact

  • Performance: Significant speed boost for the common case (code-only updates).
  • Reliability: Reduces update failures caused by transient network issues during the npm registry handshake.
  • User Experience: Removes the "stalling" feel during frequent updates for users on metered or slow connections.

Fix Action

Fixed

PR fix notes

PR #17386: feat(update): skip npm ci when node_modules already matches lockfile (#17268)

Description (problem / solution / changelog)

Summary

  • hermes update ran npm ci unconditionally in the repo root and ui-tui/ on every run, even when nothing had changed
  • npm ci deletes node_modules and re-installs from scratch — the slow path on metered or restricted networks (#17268 reports 30–60s+ stalls)
  • Skip the call when package-lock.json content already matches node_modules/.package-lock.json (npm's hidden lockfile)

The bug

_update_node_dependencies() in hermes_cli/main.py always called _run_npm_install_deterministic for every directory with a package.json. There was no "is anything actually out of date?" check before paying the npm ci cost — which is high precisely because npm ci is intentionally destructive (it removes node_modules first), so even a no-op update pays for a full registry handshake plus a clean re-install.

The TUI launcher already had a content-based lockfile comparison in _tui_need_npm_install, but it was scoped to the TUI's @hermes/ink presence guard and never called from the update path.

The fix

Extract the lockfile-content comparison into _npm_install_in_sync(root):

  • Compares package-lock.json against node_modules/.package-lock.json (npm's hidden lockfile) by content, not mtime — so git checkouts and npm rewrites that bump lockfile timestamps don't cause spurious reinstalls
  • Returns True only when every required (non-optional, non-peer) entry in the committed lockfile matches the hidden lockfile, ignoring npm-written runtime annotations like ideallyInert
  • Returns False when either lockfile is missing (defensive default — let the existing install path handle bootstrap and recovery)
  • Falls back to mtime comparison if either file is unparseable

_update_node_dependencies() calls the helper before each install and prints \"✓ {label} (already in sync with lockfile)\" when skipping. _tui_need_npm_install is now a thin wrapper around the helper plus its existing @hermes/ink presence guard — all 9 of its existing tests still pass unchanged, so the launcher's behaviour is preserved.

Test plan

  • Focused regressiontests/hermes_cli/test_update_node_dependencies_skip.py covers:
    • In-sync match (skip)
    • Missing marker / missing lockfile (run install)
    • Required package missing from marker (run install)
    • Optional/peer-only divergence (skip)
    • npm runtime annotations ignored (skip)
    • Version mismatch (run install)
    • Unparseable JSON → mtime fallback (newer marker = skip; newer lock = install)
    • End-to-end: _update_node_dependencies does NOT call _run_npm_install_deterministic when in sync; DOES call it when marker missing or packages diverge
  • Adjacent suitetests/hermes_cli/test_tui_npm_install.py (refactored function, all 9 tests pass unchanged); tests/hermes_cli/test_cmd_update.py::test_update_refreshes_repo_and_tui_node_dependencies updated to patch _npm_install_in_sync so the npm-call assertion runs regardless of whether the dev machine's node_modules happens to already match
  • Regression guard — removing the _npm_install_in_sync short-circuit from _update_node_dependencies makes test_update_skips_npm_call_when_in_sync fail with the expected "npm install failed in repo root" output instead of "already in sync with lockfile". Restoring the fix flips it back to passing.

Contract Protected

AspectBehaviour
Both lockfiles match (required entries)Skip npm ci, print (already in sync with lockfile)
Hidden lockfile missing (fresh checkout)Run install — node_modules was never populated
Committed lockfile missingRun install — defensive default; _run_npm_install_deterministic falls back to npm install
Required package missing from hidden lockRun install
Required package version differsRun install
Only optional/peer packages differSkip — npm intentionally skips these per platform
Either lockfile unparseableFall back to mtime: install only if committed lockfile is newer

Fixes #17268

Changed files

  • hermes_cli/main.py (modified, +55/-29)
  • tests/hermes_cli/test_cmd_update.py (modified, +4/-1)
  • tests/hermes_cli/test_update_node_dependencies_skip.py (added, +215/-0)
RAW_BUFFERClick to expand / collapse

Problem

During every hermes update, the updater executes npm ci unconditionally:

  • → Updating Python dependencies...
  • → Updating Node.js dependencies...

In practice, the Node.js step is the primary bottleneck. Even when a pull only contains Python logic or documentation, npm ci still triggers a registry check and metadata resolution. On slower or unstable networks (especially behind certain firewalls), this step can take 30–60+ seconds or even stall, despite the dependency tree being identical to the local state.

Proposed Solution

Implement a simple caching mechanism for the Node.js dependency state:

  1. Hash the Lockfile: Generate a hash of package-lock.json (or the relevant manifest) after a successful install.
  2. Store the Hash: Save this hash locally (e.g., in .hermes/update_cache).
  3. Pre-update Check: On subsequent updates, compare the current lockfile hash with the stored one.
    • Match: Skip npm ci and proceed to the next step.
    • Mismatch/Missing: Run npm ci and update the stored hash.

Impact

  • Performance: Significant speed boost for the common case (code-only updates).
  • Reliability: Reduces update failures caused by transient network issues during the npm registry handshake.
  • User Experience: Removes the "stalling" feel during frequent updates for users on metered or slow connections.

extent analysis

TL;DR

Implement a caching mechanism to skip unnecessary npm ci executions by comparing the hash of package-lock.json before and after updates.

Guidance

  • To mitigate the bottleneck caused by npm ci, consider implementing a simple caching mechanism as proposed, focusing on hashing and comparing the package-lock.json file.
  • Before implementing the full caching solution, verify that the package-lock.json file remains unchanged for updates that only include Python logic or documentation to ensure the cache would be effective.
  • Test the caching mechanism with different network conditions to validate its effectiveness in reducing update times and improving reliability.
  • Consider the storage location and security implications of saving the hash locally, such as using .hermes/update_cache as suggested.

Example

const fs = require('fs');
const crypto = require('crypto');

// Function to generate hash of package-lock.json
function generateHash() {
    const lockfileContent = fs.readFileSync('package-lock.json', 'utf8');
    const hash = crypto.createHash('sha256');
    hash.update(lockfileContent);
    return hash.digest('hex');
}

// Example usage
const currentHash = generateHash();
const storedHash = fs.readFileSync('.hermes/update_cache', 'utf8');
if (currentHash === storedHash) {
    console.log('Skipping npm ci');
} else {
    // Run npm ci and update the stored hash
    console.log('Running npm ci');
    // Update logic here
}

Notes

The proposed solution assumes that the package-lock.json file accurately reflects the Node.js dependency state and that hashing this file provides a reliable way to determine if the dependencies have changed. This approach may need adjustments based on specific project requirements or constraints.

Recommendation

Apply the proposed workaround by implementing the caching mechanism to skip unnecessary npm ci executions, as it offers a significant performance boost and improves reliability without requiring an upgrade to a fixed version.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

hermes - ✅(Solved) Fix feat: Skip npm install during updates when Node dependencies haven't changed [1 pull requests, 1 participants]