Assert a journey edit leaves MEMORY.md byte-identical to MemoryStore's
own §-join (no trailing-newline drift) and round-trips through
MemoryStore._read_file, so the two surfaces can never diverge on format.
learning_mutations re-implemented the §-delimited read/write that
tools/memory_tool already owns, and its writer used a plain write_text
(truncate-then-write) — reintroducing exactly the partial-file race that
MemoryStore._write_file engineered away with atomic temp-file + rename.
Reuse MemoryStore._read_file/_write_file so the format is single-sourced,
the write is atomic against concurrent readers, and journey indices stay
aligned with the graph.
The merged #55859 left the star-map NodeContextMenu import and the
canvas onContextMenu prop out of perfectionist's required order, failing
`npm run lint` in the desktop workspace. Reorder both.
TUI /journey gets d/e with confirm + $EDITOR; desktop gets a right-click
context menu with inline edit modal. Both refresh the graph after mutation.
Extract openInEditor into the shared TUI editor helper.
Bring Hermes-Setup.exe's UI onto the shared design tokens (self-contained, no
desktop-component coupling) and add two capabilities:
- design: flat stage rows (running step opaque, rest muted), neutral check /
destructive cross, running fourier-flow Loader, hairline --stroke-nous
borders, fill-less log panel; ported BrandMark (nous-girl) + HackeryButton +
Loader standalone; re-synced button variants; de-boxed success/failure.
- theme: follow the OS light/dark via the authoritative Tauri window theme
(theme.ts + onThemeChanged, core🪟allow-theme), with Nous dark seed
colors in styles.css so the --ui-*/--dt-* chain derives correctly.
- updates: split the monolithic "Updating" bar into handoff -> download ->
rebuild (+ install on macOS) stages via a shared update_stages() builder, a
live elapsed timer on the running stage, and a dev-only fake-boot preview
(gated on import.meta.env.DEV, stripped from the shipped bundle).
Adds hooks/use-composer-url-dialog.test.tsx (renderHook): @url: directive
fallback, host onAddUrl preference + clear/close, and the blank-input no-op.
First unit coverage for an extracted composer engine — previously none of this
logic was testable while welded into the DOM-coupled ChatBar.
Moves the docked↔floating state, dock/float/toggle actions, drag-gesture wiring,
and the on-screen re-clamp effect out of ChatBar into
hooks/use-composer-popout.ts, verbatim. ChatBar passes its composerRef in and
consumes the returned popout state/handlers; the secondary-window gate and the
shared persisted atom stay encapsulated in the hook.
Moves the resting-placeholder state + the conversation-change re-roll effect +
the disabled/reconnecting/starting derivation out of ChatBar into
hooks/use-composer-placeholder.ts, verbatim. The hook owns its own i18n + browse
reset; ChatBar just reads the derived string.
Moves the URL dialog's open/value state, autofocus-on-open effect, and submit
(host onAddUrl or an @url: directive) out of ChatBar into
hooks/use-composer-url-dialog.ts, verbatim. ChatBar just wires the returned
openUrlDialog into the context menu and the state into <UrlDialog>.
Moves the chat-focused Esc-cancel listener (the latest-handler ref + the
register-once window keydown effect) out of ChatBar into
hooks/use-composer-esc-cancel.ts, verbatim. Encapsulating the latest-closure ref
inside its own hook is the first of the plan's "delete the latest-closure refs"
cleanups: it's no longer a loose ref in the 1.4k-line component, just an
implementation detail of a focused side-effect hook keyed on busy/awaitingInput/
onCancel.
Moves the CodingStatusRow hand-offs (openInWorktree + branch-off / convert /
list / switch) out of ChatBar into hooks/use-composer-branch.ts, verbatim. The
hook depends only on cwd + draftRef + clearDraft (backend coupling via the
projects store); nothing about ChatBar's render. Dead projects/composer-store
imports drop out of index.tsx.
MoA sessions could not stream: the gateway streaming toggle was a no-op for
provider "moa", so users saw nothing until the entire response finished — minutes
of silence on long turns. The aggregator's reply was always fetched whole.
Root cause was twofold:
1. conversation_loop hard-disabled streaming for provider in {"copilot-acp",
"moa"} (MoA grouped with the ACP client, whose facade isn't a stream).
2. MoAChatCompletions.create() fetched the aggregator response whole via
call_llm(), which had no streaming mode.
For provider "moa", _create_request_openai_client() returns the MoAClient facade
itself, so the existing streaming consumer already calls
MoAChatCompletions.create(stream=True). We reuse that battle-tested consumer
(text-delta delivery, tool_call reassembly, stale-stream detection, non-streaming
fallback) instead of adding a parallel streaming path.
Changes:
- call_llm() gains stream/stream_options. When streaming it returns the raw SDK
stream iterator directly, bypassing _validate_llm_response and the
temperature/max_tokens/payment fallback chain (which assume a complete
response). The caller owns reassembly and fallback.
- MoAChatCompletions.create() runs the references first (unchanged), then when
stream=True returns the aggregator's raw stream, forwarding stream_options and
the consumer's per-request read timeout. stream=False is byte-identical to
before (no stream/stream_options/timeout forwarded).
- conversation_loop streams MoA only when a display/TTS consumer is present;
quiet/subagent/health-check paths keep the complete-response path.
Tests: tests/run_agent/test_moa_streaming.py — create() stream/non-stream
branches, stream_options + timeout forwarding, call_llm raw-stream return vs
validated non-stream. Existing MoA tests unchanged (20 passed).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
_build_gemini_contents emitted one contents entry per source message and
never merged adjacent same-role entries. Gemini's generateContent requires
strict user/model alternation and rejects consecutive same-role turns with
HTTP 400 ("Please ensure that multiturn requests alternate between user and
model"). A parallel tool call turns into two tool results in a row, which
become two consecutive user functionResponse contents, so every multi-tool
turn produced an unsendable history.
Fold adjacent same-role contents into one by concatenating their parts after
the per-message loop, matching the Anthropic and Bedrock converters. For a
parallel call this yields the grouped multi-functionResponse user turn Gemini
expects.
Memories are the only drillable rows, so give them the primary "clickable"
ink and demote skills (dead-ends) to the muted complement — previously the
non-openable skills wore the link-looking primary color. Flipped in both
the TUI and CLI palettes for parity.
The renderer kept a braille canvas, char-field scene, star-glyph/orbital
helpers, and seed/links params from earlier visual iterations that the
final timeline bar chart never uses. Remove them (~190 lines), simplify
the empty-state placeholder, and refresh the module + RPC docstrings to
describe what actually ships.
Add a non-selectable spacer row before each slice (except the first) so
groups breathe — the CSS `group + group { margin-top }` equivalent. The
gap counts toward the scroll window but cursor navigation skips it.
Collapse the two-step slice list → detail page into one scrollable tree:
each timeline slice is a parent header with its skills + memories nested
under ├─/└─ branch chars, ordered oldest → newest (children now sorted
chronologically in the renderer). One cursor walks the whole tree; Enter
still opens a memory's body. Drops the separate detail mode.
Skill nodes carry no body in the learning_graph payload, so opening one
dead-ended on "No additional detail recorded yet." Gate Enter/→ to nodes
with body (memories), mark those rows with a › affordance, and only show
the "open" hint when the selected row is drillable.
A single 'hermes update' / 'hermes -p' could rewrite a hand-curated config.yaml
into a near-full DEFAULT_CONFIG dump (the 'you blow up my profile config on one
tweak' reports). Root cause: migrate_config() had ~16 independent save_config()
call sites, each author deciding ad hoc whether to materialise a value, and many
persisted pure schema defaults with strip_defaults=False. Defaults already merge
transparently at read time via load_config(), so writing them is pure bloat that
also shadows future default changes (see save_config's docstring).
Architectural fix (not a per-site patch): introduce a single _persist_migration()
chokepoint that enforces one invariant — a migration may persist only values that
DIFFER from the current schema default, plus explicit removals/renames of user
data; pure defaults are never written. Every migration write (all 17 sites incl.
the version-bump finalizer) now routes through it. The invariant is mechanically
correct for all cases and verified empirically:
- pure-default seeds (timezone='', curator/auxiliary.curator blocks, interim
flag, curator.consolidate=False, empty plugins.enabled) are stripped → merged
in at read time;
- non-default values (write_approval=True, model_catalog.ttl_hours=1) preserved
via explicit-raw-path preservation;
- behaviour flips (agent.verify_on_stop=False, schema default still 'auto')
preserved because False != 'auto';
- data transforms (custom_providers->providers, stt.model relocation,
write_mode->write_approval, compression.summary_* removal, MCP-disable)
persist their removals/renames.
An explicitly user-set non-default value (e.g. matrix.require_mention: false) is
preserved across the bump.
Guard tests lock the architecture: an AST check asserts migrate_config() makes no
direct save_config() call (all writes go through _persist_migration), and a
full-range v1->latest test asserts a lean config is never dumped. Two existing
change-detector tests that froze the on-disk representation of default-valued
keys are rewritten to assert the effective value via load_config() (behaviour
contract, not snapshot).
Validation: lean v1->latest migration drops from ~567 bytes to ~196 bytes;
148 config+setup and 196 profile/curator/migrate tests pass on scripts/run_tests.sh.
Builds on the zero-match feedback fix (previous commit) to close the silent-hang
symptom: when memory is at capacity, a failed `add`/`replace`/`remove`
consolidation could loop the whole turn to iteration-budget exhaustion and
deliver no user-facing reply.
#41755 turned the at-capacity overflow error into a *commanded* in-turn retry
("...then retry this add — all in this turn"); combined with the fragile
substring-only `replace`/`remove` matching (LLMs can't reliably re-quote a long
entry verbatim), the model loops add↔replace on inexact guesses until the turn
dies. The existing tool_guardrails halt would catch this, but hard_stop_enabled
is opt-in (off by default), so a default install still hangs.
This fixes it at the memory layer without changing global guardrail behavior:
- MemoryStore tracks per-turn consolidation failures; after a cap (3) it drops
the "retry in this turn" instruction and returns a terminal "leave memory
unchanged, continue your reply" result, so a failed memory side effect can
never block the turn's reply.
- The counter resets on any successful write (progress) and at each turn
boundary (turn_context.reset_consolidation_failures, guarded via getattr so
plugin memory stores without the method are a no-op).
Co-authored-by: liuhao1024 <sunsky.lau@gmail.com>
- replace() and remove() now return entry previews and current_entries
when no entry matches old_text, matching the multi-match and add-limit
error behavior
- add() limit error also now returns previews for consistency
- Agent can self-correct after a failed replace/remove instead of looping
blindly until turn budget is exhausted with no user response
Probe the projects.* RPC surface, block create with a clear update hint,
and avoid the raw "unknown method" toast. Includes i18n for en, zh, ja,
and zh-hant.
FixesNousResearch/hermes-agent#54999
The cherry-picked fix added explicit-kwarg and top-level image_gen.model
resolution but left _resolve_model / _resolve_model_chain docstrings stating
the old 'env override -> config -> DEFAULT_MODEL' order. Document the full
precedence (explicit kwarg -> env -> scoped -> top-level -> default chain) to
match the sibling krea/openai providers.
hermes tools persists the selected model to image_gen.model, but the
OpenRouter-compatible provider only read scoped image_gen.<provider>.model
and ignored the dispatch model kwarg — so Nous users always hit the default
quality-first chain and fell back to Gemini.
Bootstrap and desktop updates run install.ps1/install.sh, which aborted
with exit 128 when the managed checkout had diverged from origin/main.
Mirror the hermes update recovery path: reset to origin/$BRANCH instead
of failing the repository stage.
Follow-up to the per-tool availability derivation: `_snapshot_toolset_checks`
and `_evaluate_toolset_check` had no remaining callers once the four
availability surfaces switched to `_toolset_has_exposable_tools`. Remove both,
drop the no-op `quiet` param from the new helper, and document why
`_toolset_checks` is still written (banner.py reads it via TOOLSET_REQUIREMENTS
to classify unavailable toolsets as lazy-init vs disabled).
Regression for #54820: a desktop-only helper with a failing check_fn must
not mark the whole terminal toolset unavailable when terminal/process
still pass their per-tool gates.
Doctor and banner used the first check_fn registered for a toolset, so
desktop-only read_terminal gated the whole terminal toolset even though
terminal and process still expose at runtime.
Fixes#54820
Interrupting the agent while an approval/clarify/sudo/secret prompt is up
left the overlay state dict set with no thread servicing it. The prompt's
worker thread is torn down on interrupt, but read_only (gated on
_command_running) plus the keypress filter kept the CLI input locked until
the prompt's own timeout expired — the terminal appeared frozen.
Drain and clear all four input-blocking overlays on interrupt via a single
helper (_clear_active_overlays_for_interrupt): approval -> deny,
clarify/sudo/secret -> cancel, each guarded so a dead queue can't block the
others; sudo restores the pre-modal draft. Wired into all three interrupt
paths — new-message interrupt, Ctrl+C, and Ctrl+Q. Blocking overlays now
clear AND fall through so one keypress both clears a stale overlay and
interrupts a still-running agent; the /model picker and slash-confirm
foreground prompts keep their cancel-and-return behavior.
Closes#13618.
Sibling of #15795's context_compressor fix. agent/moa_loop.py used the
same response.choices[0].message.content access; while wrapped in
try/except (so no crash), a dict/str-shaped message silently returned
empty. Coerce defensively so the content is actually extracted.