test_docker_network_config.py landed on main after the #58489 revert and
stubbed docker ps with the 2-field ID\tState format. The re-landed
egress-aware reuse probe requests ID\tState\tEgressLabel when egress is
off, so the fake line failed to parse and the reuse path never fired.
Fixture-only change; production behavior is unchanged.
The salvaged #61978 covers tui_gateway/server.py. The crash reported on
Jul 24 came from a sibling site it doesn't touch: the desktop update
panel's _recent_upstream_commits() in hermes_cli/web_server.py runs
git log with text=True and no encoding. Commit 84db32484f put a bug
emoji (UTF-8 f0 9f 90 9b) in a subject on main; byte 0x90 is undefined
in cp1252, so every Windows desktop install behind that commit crashed
in subprocess._readerthread during the update check (#52649).
Guard every text=True capture site in the desktop-backend process with
encoding='utf-8', errors='replace':
- hermes_cli/web_server.py: git log update panel, memory-provider setup
runner, WhatsApp bridge npm install, docker probe
- hermes_cli/banner.py: all 5 git sites (update check runs at startup)
- tui_gateway/host_supervisor.py: build-sha probe, ps probe, compute-host
Popen drain threads
- tui_gateway/compute_host.py: build-sha probe, ps rss probe
The gateway startup maintenance block gained a maybe_auto_archive call in
the same provably-off-loop __init__ site as maybe_auto_prune_and_vacuum;
bump the reviewed sync-escape count from 3 to 4.
Sessions settings gain an "Auto-archive stale chats" toggle with a
configurable idle threshold, persisted to sessions.* in config.yaml so
the backend sweep owns the policy. Sidebar pins (localStorage) are
mirrored to the backend pinned flag at boot and on every change —
pre-existing pins migrate transparently — so the sweep can never hide a
pinned chat.
New sessions.auto_archive / auto_archive_days config: soft-hide (never
delete) sessions with no activity for N days, aging on last activity
rather than creation so an old-but-active chat is spared. Sweeps are
throttled through state_meta and fire from CLI startup, gateway startup
+ hourly housekeeping, and the serve/dashboard backend (opportunistic
on session list + an hourly lifespan ticker), so every surface honours
one setting.
A new pinned column (declaratively migrated) exempts sessions from the
sweep; PATCH /api/sessions/{id} accepts pinned and flips the whole
compression lineage as a unit, mirroring set_session_archived.
Group the flat recents list and entered-project lanes by recency: an
unlabelled head of the newest run of sessions (cut at a real break in
activity, sized toward the most recent handful), then one divider per
coarse calendar range — Earlier today / Yesterday / Earlier this week /
Last week / Earlier this month / month / month + year. Empty ranges are
skipped, the first rendered group is never labelled, branch clusters
never split, and hand-ordered lists / pinned / project previews stay
divider-free.
Nine bundled and optional skills had stale flags, install URLs, packages, and paths. Verified each against upstream and corrected:
- vllm: removed bogus --enable-metrics/--metrics-port (metrics at /metrics on API port); --speculative-model -> --speculative-config; canonical HF model IDs
- lm-evaluation-harness: --tasks list -> lm-eval ls tasks; --allow_code_execution -> --confirm_run_unsafe_code
- weights-and-biases: wandb.keras import removed -> wandb.integration.keras (WandbMetricsLogger); log_uniform -> log_uniform_values for raw values
- huggingface-hub: upload-large-folder now deprecated; hf papers list -> ls
- openhue: Linux install 404 -> openhue_Linux_x86_64.tar.gz tarball (release repo openhue/openhue-cli, v0.24)
- apple-notes: memo notes -a is a bare flag, no positional title
- excalidraw: upload.py path skills/diagramming/... -> skills/creative/...
- searxng-search: removed Method 3 (searxng-data pip package is a PyPI 404)
- sketch: noted get-shit-done upstream is archived/unmaintained
The design-md skill documented the Apr 2026 (0.1.x) CLI behavior, which
has since drifted:
- Lint rules: the skill listed 7 rules that no longer exist by those
names (duplicate-section, invalid-color, wcag-contrast,
unknown-component-property); the 0.3.0 linter runs 9 rules
(contrast-ratio, orphaned-tokens, missing-primary, missing-typography,
section-order, unknown-key, token-summary, missing-sections,
broken-ref). Verified against live lint output.
- Colors: any CSS color is now valid (oklch/rgb/named), not hex-only.
- Export: json-tailwind (v3) + css-tailwind (Tailwind v4 @theme CSS)
formats; 'tailwind' is a back-compat alias. New exit-code semantics
(export exits 0 regardless of source lint findings).
- Section order / duplicate headings are lint warnings, not file
rejection (verified: duplicate + out-of-order sections exit 0).
- Windows: documented the designmd dot-free bin alias (the design.md
bin name collides with the .md file association); skill declares
platforms: [windows].
- New pitfall: typography sub-property typos (fontwight) are silently
dropped with no finding as of 0.3.0.
All claims verified by running @google/design.md 0.3.0 live (lint,
export, duplicate-section, oklch token, starter template lints clean).
Docs page regenerated via generate-skill-docs.py.
Four coding-agent CLI skills drifted from their live CLIs. Verified against live --help/npm and corrected:
- codex: --full-auto deprecated -> --sandbox workspace-write; --yolo -> --dangerously-bypass-approvals-and-sandbox (yolo kept as noted alias)
- claude-code: --effort levels low/medium/high/xhigh/max (dropped removed 'auto', added 'xhigh'); fixed stray table cell
- grok: --session-id is UUID-only for new sessions (cannot resume by name); rewrote the Session Continuation example; noted --max-turns now exists
- blackbox: wrong npm package (@blackboxai/cli is unrelated) -> @blackbox_ai/blackbox-cli; removed dead source-repo link and phantom session/info subcommands
session.branch wrote the child ROW into the parent's profile db but
built the live agent with the launch defaults: _make_agent fell back to
_get_db() and no HERMES_HOME override was active. The branched agent's
own message flushes — and any later compression rotation it performed —
therefore landed back on the launch profile, splitting the lineage one
turn after the branch. Mirror session.create/resume: open the parent
profile's SessionDB for the agent and hold the home override across the
build, so config/skills/memory resolve to the profile too.
Spotted in #70605's sibling implementation of the same fix.
Co-authored-by: HexLab98 <liruixinch@outlook.com>
The blanket MAX_DESCRIPTION_LENGTH=1024->60 change is narrowed:
create-time validation now rejects new skills whose description
exceeds SKILL_PROMPT_DESC_LIMIT (60) with actionable guidance, while
edit/patch paths stay permissive (warning via system_prompt_preview)
so existing over-limit skills remain maintainable. Runtime display
truncation in skills_tool is left at 1024 (display behavior is a
separate concern from authoring validation).
Boundary tests: 60 accepted, 61 rejected at create; edit/patch on
over-budget skills still succeed.
MAX_DESCRIPTION_LENGTH was set to 1024, but the documented skill-
authoring standard specifies <=60 characters. The model generates
descriptions up to 202 chars because the validation allows 1024.
Lower MAX_DESCRIPTION_LENGTH from 1024 to 60 to match the documented
standard. The system-prompt skill index already truncates to 60 chars,
so over-length descriptions lose their routing signal past char 60.
Fixes#52367
Widening pass on top of the #38820 salvage: a full-graph audit of every
SKILL.md (bundled + optional) found 13 more references to skills that
no longer exist. Classes:
- deleted in the 38d3c49aaf bundled-skill cleanup: generative-widgets,
spotify, cloudflared-quick-tunnel, webhook-subscriptions,
debugging-hermes-tui-commands -> dropped
- native-mcp absorbed into the hermes-agent hub skill -> re-pointed
- toolset names that were never skills: browser, image_gen -> dropped
Audit now reports zero broken related_skills references.
Salvaged from PR #38820 by @bedirhancode, re-applied at current skill
locations (obliteratus and s6 moved to optional-skills/ since the PR):
- research-paper-writing: drop ml-paper-writing (never existed)
- touchdesigner-mcp: drop native-mcp (consolidated) + hermes-video (never existed)
- obliteratus: vllm -> serving-llms-vllm, gguf -> llama-cpp
- s6-container-supervision: drop hermes-agent-dev (not a repo skill)
4 of the original 8 hunks were dropped: heartmula already fixed in
#70453; native-mcp SKILL.md deleted from main; architecture-diagram and
comfyui hunks removed refs to concept-diagrams and
stable-diffusion-image-generation, which are valid optional skills.
Auto-gen page slugs, catalog rows/paths, sidebar entries, and zh-Hans
mirrors follow the directory renames. Also updates the install path
official/creative/audiocraft -> official/creative/audiocraft-audio-generation
in the songwriting-and-ai-music pointer section.
Salvaged from PR #42788 by @Love-JourneY, re-applied at current locations
(audiocraft and segment-anything have since moved to optional-skills/):
- skills/mlops/inference/vllm -> serving-llms-vllm
- skills/mlops/evaluation/lm-evaluation-harness -> evaluating-llms-harness
- optional-skills/mlops/models/segment-anything -> segment-anything-model
- optional-skills/creative/audiocraft -> audiocraft-audio-generation
Directory name != frontmatter name breaks skill_view() lookup by dir
name and causes hermes update sync re-seeding duplicates (#42786).
The authoring guide calls this out as Pitfall #8.
Fixes#42786
RFC 8628 §3.2 makes the device-authorization `interval` optional with a
client-side default of 5 seconds. request_device_code required it, so a
compliant AS that omitted it hit the malformed-response path and the flow
could never complete. Fall back to 5s and cover it with a regression test.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds a device authorization grant flow alongside the existing loopback
OAuth flow, so `hermes setup` can connect to Honcho cloud from SSH and
other no-browser environments.
- oauth.py: new HTTP seams — _http_post_form_status (non-raising, since
RFC 8628 polling reads the OAuth error off a 400) and _http_get_json
for the RFC 8414 metadata probe
- oauth_flow.py: DeviceCode, request_device_code, poll_for_token with
slow_down backoff (+5s, capped at 60s) bounded by expires_in, typed
errors (AccessDenied, DeviceCodeExpired, AuthorizationTimeout), and
supports_device_login (fail-closed metadata gate); device flow ends in
the same install_grant tail as loopback so refresh/status work
unchanged
- oauth_flow.py: loopback callback now serves a "sign-in was not
completed" page on consent cancel instead of the success page
- cli.py: cloud menu offers oauth / device / apikey; the device option
only appears when the host advertises the grant, and becomes the
default when no browser is detected
- 18 new tests covering the full flow against a local fake AS, backoff
schedule, error mapping, deadline bound, metadata gate, and wizard
branches
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The multiplex cron path only used use_cron_store() to scope storage paths
(jobs.json, heartbeat files), but _get_lock_paths() and the agent execution
path in cron/scheduler.py resolve via _get_hermes_home() → get_hermes_home()
which checks _HERMES_HOME_OVERRIDE, a separate ContextVar. Without
set_hermes_home_override(), the .tick.lock, config.yaml, .env, and secrets
all resolved to the default profile instead of the per-profile home.
This matches the web_server.py pattern (line 11994) which sets both
set_hermes_home_override(home) AND use_cron_store(home), and the
_profile_runtime_scope pattern used for the multiplexed inbound path.
Found via 3-agent parallel review of salvaged PR #69529.
Under multiplex_profiles, the gateway starts a single InProcessCronScheduler
bound to the process-global HERMES_HOME (the default profile's home), so
only that profile's cron/jobs.json is ticked. A job registered from a
secondary-profile session lands in <profile>/cron/jobs.json, reports a valid
next_run_at — and never fires.
Changes:
1. cron/scheduler_provider.py — InProcessCronScheduler.start() now accepts
an optional profile_homes kwarg (list of (name, Path) tuples). When set,
_start_multiplex() iterates tick() over each profile home using
use_cron_store(), so every served profile's cron store is ticked on
every tick cycle. Heartbeats and interrupted-execution recovery are also
scoped per profile via use_cron_store().
2. gateway/run.py — start_gateway() now resolves profiles_to_serve(multiplex=True)
when multiplex_profiles is on and passes them to the cron scheduler as
profile_homes. Only applies to InProcessCronScheduler (the built-in);
external providers are unchanged.
3. cron/jobs.py — record_ticker_heartbeat(), get_ticker_heartbeat_age(), and
get_ticker_success_age() now resolve paths via _current_cron_store()
instead of module-level TICKER_HEARTBEAT_FILE / TICKER_SUCCESS_FILE
constants. This makes heartbeats correctly scoped per profile, so
'hermes cron status' reflects liveness for every profile independently
under multiplex_profiles.
4. tests/cron/test_scheduler_provider.py — two new tests:
- test_multiplex_ticker_ticks_each_profile_once: verifies tick() is called
once per profile per tick cycle.
- test_multiplex_heartbeat_scoped_per_profile: verifies heartbeat files
are written to each profile's cron store.
Widens the salvaged .env injection fix (#50315) to the sibling site it
missed: hermes_cli/memory_setup.py::_write_env_vars is the near-identical
core writer the openviking plugin's copy was forked from, is fed directly
by interactive _prompt() (pasted API keys), and is reused by other memory
plugins (e.g. supermemory imports it). A pasted secret with an embedded
CR/LF injected an arbitrary extra KEY=VALUE line on the next read.
Same _env_line_safe() treatment as the plugin writer (strip every
str.splitlines() separator + NUL), matching config.save_env_value's
existing newline strip. Mutation-checked: reverting the sanitizer makes
the new regression tests fail.
Follow-up hardening on the salvaged _ensure_client() (#21130 fix):
- Failed-config cooldown: after a refresh attempt fails for a given
resolved config, skip re-probing for 30s. Previously every provider
access against a down endpoint paid a 3s health probe under
_client_refresh_lock and emitted a warning (2+ per turn, some on
user-facing threads: prefetch, tool calls, session end). Retries
still happen after the cooldown or immediately when config changes,
and the log message now says so instead of the false 'disabled until
config changes'.
- Atomic connection snapshot: _conn_snapshot (5-tuple, single
assignment) is published only after a health check passes.
_new_client() and on_memory_write's writer read it as one load, so
background writers can no longer observe a torn mix of old/new
identity fields mid-refresh or target an endpoint that never passed
health. Field writes in _ensure_client_locked keep tracking the
attempted config for the unchanged-config dedupe.
- _env_refresh_enabled moves to the top of initialize(): an exception
mid-initialize (swallowed by MemoryManager) can no longer leave the
provider silently stuck in never-refresh mode.
- _search_prefetch_context reuses _new_client() and degrades to ''
on construction failure instead of propagating.
Mutation-checked: neutering the cooldown or publishing the snapshot on
failed health makes the new regression tests fail.
Avoid spawning multiple local OpenViking server processes while a runtime autostart waiter is already active. Remote endpoints still retry on later accesses because they do not install a local waiter.
Route refreshed unreachable local OpenViking configs through the existing runtime recovery path so /reload can attach to a locally starting server instead of disabling memory until restart.
(cherry picked from commit 040e18ad90)
The _needs_trusted_identity_retry method was hard-coding specific
server-side error strings to detect when a request failed due to
missing X-OpenViking-Account / X-OpenViking-User headers. Each new
server-side error variant required another string added to the client.
Replace the string enumeration with a structural match: the error
message mentions one of the tenant headers AND the HTTP status is 400.
This covers all current error variants:
- "Trusted mode requests must include X-OpenViking-Account and User"
- "ROOT requests to tenant-scoped APIs must include X-OpenViking-Account"
- "Trusted mode requests must include X-OpenViking-Account."
- "Trusted mode requests must include X-OpenViking-User."
The 400 status guard avoids false-positives on 403 errors such as
"USER API keys cannot override X-OpenViking-User", which must not
trigger a retry.
All 176 existing tests pass.
(cherry picked from commit 5a24d6766c)
`OpenVikingMemoryProvider.shutdown()` joins in-flight writers, deferred-commit
threads, and prefetch threads, but not `_runtime_start_thread` — the tracked
`daemon=True` waiter that runs `_finish_runtime_openviking_start`, which blocks
on network health probes (`_wait_for_openviking_health` polling + a
`_VikingClient.health()` request).
If the local OpenViking runtime is slow or unreachable, that waiter can still
be blocked in network I/O at interpreter exit. CPython then forcibly kills it
during `Py_FinalizeEx` (`PyThread_exit_thread` -> `__pthread_unwind` ->
`abort()`), producing SIGABRT (exit 134) with no traceback — the same daemon-
thread-at-exit failure class fixed for the Honcho provider.
Fix:
- `shutdown()` now joins `_runtime_start_thread` (timeout-bounded) alongside the
other tracked threads.
- `_wait_for_openviking_health()` gains a `should_stop` callback; the waiter
passes `lambda: self._shutting_down` so the poll loop bails out promptly once
`shutdown()` flips the flag, instead of lingering up to the 60s autostart
timeout and timing out the join (which would leave the thread alive).
- Add tests/plugins/memory/test_openviking_shutdown.py covering the short-circuit
and the shutdown-joins-runtime-thread behaviour.
(cherry picked from commit 5471ec7021)
initialize() snapshots OPENVIKING_* into the provider once, so /reload
(which only updates os.environ) leaves viking_* tools running against
stale auth — users have to restart hermes to pick up keys added to
~/.hermes/.env after startup.
Add _ensure_client(), which re-resolves the connection settings via the
same _resolve_connection_settings/_load_hermes_openviking_config path
initialize() uses and rebuilds + health-checks the client only when an
OPENVIKING_* value actually changed; otherwise it reuses the cached
client so the hot path stays at one dict comparison with no network
calls. Every `if not self._client:` guard in system_prompt_block,
queue_prefetch, sync_turn, on_session_end, on_memory_write and
handle_tool_call now goes through it.
Refreshing is gated behind a flag set at the end of initialize() so the
baseline is established before any env re-resolution happens — callers
that wire up a client directly keep the existing client untouched.
Refs #21130
(cherry picked from commit b694d21b7c)
`_write_env_vars` in the OpenViking memory provider interpolates each
secret straight into a `KEY=VALUE` line, but the values only ever pass
through `_clean_config_value`, whose `value.strip()` trims surrounding
whitespace and leaves internal CR/LF intact. Because the file is strictly
line-oriented and is re-read via `read_text().splitlines()`, a value that
carries an embedded newline spills onto a second physical line, and the
tail is re-parsed as an independent `KEY=VALUE` entry on the next round
trip. A secret pasted with a trailing record (e.g. an `OPENVIKING_API_KEY`
copied with an extra line) therefore injects an arbitrary additional
variable into the persisted credentials file and silently corrupts it.
The fix neutralizes the line terminators at the single chokepoint where
values reach the file. A small `_env_line_safe` helper strips `\r`, `\n`,
and the NUL byte from each value, and both write sites in `_write_env_vars`
(the existing-key update branch and the appended-key branch) route through
it, so a value can only ever occupy the single line it is written on.
## What does this PR do?
Hardens the OpenViking memory provider's `.env` writer so a malformed or
pasted secret value can no longer break out of its `KEY=VALUE` line and
inject a rogue variable into the profile-scoped credentials file.
## Related Issue
N/A
## Type of Change
- [x] 🐛 Bug fix (non-breaking change that fixes an issue)
## Changes Made
- `plugins/memory/openviking/__init__.py`: add `_env_line_safe()` which
removes `\r`, `\n`, and `\x00` from a value, and apply it to both the
updated-key and appended-key write branches in `_write_env_vars()`.
- `tests/plugins/memory/test_openviking_provider.py`: add two regression
tests covering a fresh write and an in-place key update with embedded
CR/LF, asserting no injected line survives the read-back.
## How to Test
1. Run the targeted tests:
`pytest tests/plugins/memory/test_openviking_provider.py -k env_writer -q`
2. Reverting the `_env_line_safe` sanitization makes
`test_openviking_env_writer_strips_embedded_newlines_in_values` and
`test_openviking_env_writer_strips_newlines_when_updating_existing_key`
fail with a rogue `INJECTED_KEY=`/`ROGUE=1` line appearing in the file,
confirming the tests pin the bug.
3. `ruff check plugins/memory/openviking/__init__.py` and
`python scripts/check-windows-footguns.py plugins/memory/openviking/__init__.py`
both pass.
## Checklist
### Code
- [x] I've read the Contributing Guide
- [x] My commit messages follow Conventional Commits
- [x] I searched for existing PRs to make sure this isn't a duplicate
- [x] My PR contains only changes related to this fix
- [x] I've run the relevant tests and they pass
- [x] I've added tests for my changes (required for bug fixes)
- [x] I've tested on my platform: macOS 15 (Darwin 25.5)
### Documentation & Housekeeping
- [x] I've updated relevant documentation (docstrings) — or N/A
- [x] I've updated `cli-config.yaml.example` if I added/changed config keys — N/A
- [x] I've updated `CONTRIBUTING.md` or `AGENTS.md` if I changed architecture or workflows — N/A
- [x] I've considered cross-platform impact (strips CR as well as LF) — done
- [x] I've updated tool descriptions/schemas if I changed tool behavior — N/A
(cherry picked from commit f29dd2df84)
profile_name was only written on the agent's initial lazy create
(e8b7ce8c1); every parented child row — compression rotation, TUI
/branch, desktop branch first-persist — was created without it. A
non-default profile's lineage therefore turned NULL on its first
compression or branch and aggregated as "default" in unified session
lists, completing the cross-profile session-jump.
Fix the class at the DB layer: _insert_session_row's parent backfill now
COALESCEs profile_name from the parent alongside cwd/git_* (#64709
pattern), so any parented child inherits its lineage's owning profile.
Stamp it explicitly at the three create sites as well — compression
rotation (mirroring _ensure_db_session), TUI session.branch, and the
TUI first-prompt row persist — so rows are self-describing even when the
parent row predates the profile_name column.
forkBranch called session.create without a profile, so in app-global
remote mode (one backend serving every profile) a branch of a
non-default-profile chat silently landed on the launch profile — the
"session jumps between profiles after branching" bug. New chats already
carry their profile (desktopSessionCreateParams, #39993); branching was
the remaining create callsite without it.
Thread the parent's owning profile through both branch entry points:
branchStoredSession already resolved it (#67603) but dropped it before
the create; branchCurrentSession now resolves the open chat's profile
via the same cache → active → cross-profile ladder as resume. forkBranch
swaps the live gateway onto that profile and passes it on session.create,
which also makes the optimistic sidebar row's profile stamp correct.
Supersedes #40788 — same invariant, rebuilt for the current forkBranch/
branchStoredSession split.
Co-authored-by: Dusk1e <yusufalweshdemir@gmail.com>