The repo's .npmrc sets engine-strict=true and package.json pins
engines.npm, so an npm outside that range aborts every npm ci /
npm install we run inside the checkout:
npm error code EBADENGINE
npm error notsup Required: {"npm":"<11.10.0 || >=12.0.0"}
npm error notsup Actual: {"npm":"11.10.0"}
Our callers made that worse: _run_npm_install_deterministic sees
`npm ci` fail and falls through to `npm install`, which fails
identically, so the user got a buried EBADENGINE and no remedy.
React to the failure instead of predicting it. npm states the
required range in its own error, so there is no need for a version
probe on the happy path or a semver range matcher — the recovery
reads the constraint out of the output it just produced, upgrades,
and retries once.
Scope is deliberately narrow. Hermes only upgrades an npm inside its
own managed Node tree ($HERMES_HOME/node), installing with --prefix
so bin/npm keeps resolving to the upgraded lib/node_modules/npm; a
managed install writes prefix=~/.local into node/etc/npmrc, so
without the override the "upgrade" would land elsewhere while the
managed npm stayed stale. A system / nvm / brew / Nix npm belongs to
the user, so that case prints the exact command and lets the original
failure stand.
The upgrade runs from a temp cwd with npm_config_min_release_age=0,
otherwise the checkout's own min-release-age gate would refuse the
npm release we need.
_run_npm_install_deterministic's capture_output=False callers (the
desktop install) streamed npm output and returned stderr=None, which
would leave the recovery nothing to read — stderr is now teed, so
live output is unchanged and the text stays inspectable.
Verified end to end against real npm binaries on copies of a managed
tree: managed npm 11.10.0 -> EBADENGINE -> upgraded to 12.0.2 ->
retry exits 0; a foreign npm 11.10.0 hard-fails with the manual
command and is left untouched.
pin brace-expansion to 5.0.8
update concurrently to 10.0.4
update electron-builder to 26.15.3
update eslint to 10.8.0
update eslint-plugin-perfectionist to 5.10.0
update @assistant-ui/react to 0.15.0
update @assistant-ui/react-streamdown to 0.3.8
update radix-ui to 1.6.7
update react-router-dom to react-router@8.3.0 - react-router-dom is no longer a standalone package, it just reexports react-router
remove @radix-ui/react-slot: we import this from `radix-ui`
remove eslint-plugin-react: we imported it, but never actually used it!
The statusbar is now opt-in. Existing users with a stored preference
keep their choice; new users get a clean bottom edge. The way back is
the view.toggleStatusbar keybind or the ⌘K row, unchanged.
Dated snapshot of deepseek-v4-flash, live on both provider endpoints
(verified via OpenRouter /api/v1/models and Nous portal /v1/models).
Context (1M via deepseek-v4-flash prefix match), reasoning stale-timeout
floor (600s), and pricing (official_models_api live billing on both
routes) all resolve without new entries. model-catalog.json regenerated
via scripts/build_model_catalog.py.
The chronos cron-fire verifier constructed PyJWKClient without explicit
headers, so its JWKS fetch to the NAS portal hit the same WAF 403 the
dashboard-auth providers already guard against. It reaches the same
portal issuer, so it's the same bug class — mirror the fix here and add
a constructor-contract regression test.
Co-authored-by: James Hodgkinson <james@terminaloutcomes.com>
The Nous PyJWKClient was constructed without explicit headers, while the
self_hosted provider already sends Accept + User-Agent. Without them the
Portal WAF can block the JWKS fetch, so the same failure mode remained for
the Nous dashboard-auth route. Mirror the self_hosted fix and add a
constructor-contract regression test.
The resume wait notice cleared on the first nonempty raw PTY frame, but
the terminal is written sanitizer.next(text). The sanitizer collapses an
erase-only, all-newline, or partial-CSI resume frame to "", so a control-
only first frame hid the notice while xterm was still blank.
Gate hydration completion on the rendered payload actually written to the
terminal, and cover the control-only-first-frame case with a regression
test over the real sanitizer.
Co-authored-by: teknium1 <teknium@nousresearch.com>
Cover the blank TUI + blinking-cursor window on session resume, then hide
the notice as soon as the first real PTY payload arrives so history can
stream in visibly.
Follow-up to PR #70238. Remove 'free-response channel' and
'authorization' jargon from the model-facing prompt. Collapse the
triple-negative 'do not ask / do not reject / do not stay silent'
into a single directive. ~70 tokens vs ~175 in the contributor's
version, same semantics.
Phase 2 review findings on the salvage branch:
C1 (critical): batch and micro summary markers share
COMPRESSED_SUMMARY_METADATA_KEY, and compress() never reset micro state.
After micro absorbed exchanges 1..k, a batch compaction summarizing
1..m (m>k) could fire; the next micro pass's supersede then dropped the
batch marker (whose content the stale rolling summary does NOT contain)
and archive_and_compact immediately made the loss durable. Defrag had
the same hazard: it rewrote "the newest marker" even if that was a
batch marker. Empirically confirmed with a probe (batch marker content
destroyed in one pass).
Fix, three parts:
- Micro-created markers now carry MICRO_COMPACT_MARKER_KEY; supersede
and defrag only ever touch micro-tagged markers. Rehydration in
_resolve_compact_cursor tags the marker it absorbs (containment
proof), which safely covers adopting a batch marker as the new
rolling base after a reset.
- compress() success path resets micro rolling summary/cursor state so
a stale summary can never claim cumulativeness over a batch marker.
- Regression tests for both directions plus the reset.
W4: _splice_micro_compact_result no longer strips _db_persisted stamps
from surviving messages. Micro archives in place under the SAME session
id (unlike batch's child-session rotation, #57491), so surviving stamps
are accurate; stripping them meant an archive_and_compact failure left
every previously-persisted message unstamped and the next append-only
flush re-inserted them all as duplicate active rows.
W5: finalize_turn micro gate now checks agent._persist_disabled —
persistence-isolated fork agents (background review) must not burn an
aux call per review turn, and must never archive_and_compact the
canonical session rows if their compressor ever gains a DB binding.
W1: _serialize_one_exchange now delegates to _serialize_for_summary
(was a ~70-line near-verbatim copy; one serializer, one place to fix).
S4: _find_one_exchange boundary guard rejects only assistant/tool
boundaries (the actual alternation hazard) instead of requiring user —
a stray mid-list system/injected message can no longer wedge the
cursor forever.
5 new regression tests; 38 micro/prune tests, 400 compression-suite
tests, 61 finalize/persist tests pass; ruff clean.