3929 Commits

Author SHA1 Message Date
Brooklyn Nicholson
929de15c73 docs(desktop): correct WorkspacePageHeaderControl older-host note
Some checks are pending
Deploy Site / deploy-vercel (push) Waiting to run
Deploy Site / deploy-docs (push) Waiting to run
auto-fix lint issues & formatting / Generate eslint --fix patch (push) Waiting to run
auto-fix lint issues & formatting / Apply patch (push) Blocked by required conditions
A named import of a missing SDK export fails to link through the runtime
shim, so the plugin never loads; it is not undefined. Point authors at a
namespace-import feature-detect or the raw Contribute form.

Refs #123597

Originally authored by Justin Haynes (@jhaynes).
2026-09-27 13:18:46 -05:00
Brooklyn Nicholson
aa25f9e85f fix(desktop): Kanban board switcher outside the full-page layout
A Kanban board opened in a split route tile rendered no board switcher:
the board contributed it to WORKSPACE_PAGE_HEADER_AREA unconditionally,
and only the workspace pane paints that area. The tile's contribution
also leaked into another page's header and shared its id with the full
page's, so closing the tile removed the page's switcher.

Add WorkspacePageHeaderControl (exported via the plugin SDK). The
workspace pane's render provides a private host context; inside it the
control projects into the page header, anywhere else it renders inline.
The board mounts BoardSwitcher once, through it, in its own header row.

Fixes #123597

Originally authored by Justin Haynes (@jhaynes).
2026-09-27 13:18:46 -05:00
Adolanium
d1167fdff7 fix(desktop): match custom:<key> providers in the settings and bot model pickers
model.info and saved profiles report a user-defined provider as custom:<key>, but the catalog row uses the bare key as its slug. Settings > Model and the Bot Mode picker compared the two with ===, so a saved custom provider never found its row. Settings showed a duplicate custom:<key> entry and a Set up provider button, and the bot editor fell back to the manual form.

Both now match rows with catalogProviderMatches, like the composer picker already does. Settings uses a small findCatalogProvider helper for every row lookup, including the aux and MoA slots and the endpoint passed on Set to main. catalogProviderMatches is now exported through the plugin SDK so the bot picker can use it.
2026-09-27 12:49:56 -05:00
shali10
40523600b0 fix(sessions): refuse to delete a session row a live turn still owns (#123583)
Refactor entry-side deletion refusal to execute in-transaction via
`_write_guards_reject(conn, sid)` (#123583), per maintainer review:

- Underlying `delete_session` and `delete_sessions` now accept an opt-in
  kwarg `exclude_active_write_guards=True` running inside `_do` write
  transaction, eliminating the race condition where a turn acquires the lease
  between check and delete.
- Raises `SessionActiveWriteGuardError` when refusing single delete, leaving
  the row untouched; `delete_sessions` atomically skips active rows.
- Checks both active turn leases and compression locks via the existing
  reclaim-aware `_write_guards_reject` helper.
- Covers all user-facing delete sinks:
  * Web `DELETE /api/sessions/{id}` -> 409 Conflict
  * Web `POST /api/sessions/bulk-delete` -> skips active rows
  * Web / CLI `prune` -> passes `exclude_active_write_guards=True` so lineage
    parents of active conversations are not pruned
  * API Server `DELETE /api/sessions/{id}` -> 409 session_active_turn
  * CLI `hermes sessions delete` & `export --delete-after-verified` -> exits 1
  * CLI browse picker -> refuses active delete
  * TUI Gateway `session.delete` -> 4023 error
- Conforms to rubric with 2 targeted invariant tests in
  `tests/hermes_state/test_delete_session_write_guards.py`.
- Updates user guide and web dashboard docs for 409 / exit 1.

(cherry picked from commit 2c037a7a79dc211b49bacc72e3140951ccf900cf)
2026-09-27 20:49:04 +05:30
kshitijk4poor
1ec84a2dae fix(simplex): document contactId-only allowlist and warn on name entries
After #44729 SIMPLEX_ALLOWED_USERS matches only the numeric contactId, but
the docs still told operators display names work, and existing name
entries would silently stop matching. Update the docs and log a one-time
warning at first connect listing non-numeric entries that are now ignored.
2026-09-27 20:47:41 +05:30
kshitijk4poor
f6ce8bb23b fix(context): assemble compaction head/tail from the pruned copy (#61932)
The salvaged lossless-history change rebuilt the carried head/tail from
canonical history, which undid _pressure_demote_tail's tool-result
shrinking and re-broke #61932 (an all-oversized tail could no longer
compress). Pruning no longer rewrites tool_calls, so the pruned copy's
arguments are already byte-identical to canonical history: assemble the
head and tail from the pruned copy, keeping tool-result demotions and
exact tool-call arguments at once. Docs updated to match.
2026-09-27 18:38:58 +05:30
JoaoMarcos44
a7baa5f5eb fix(context): keep compaction history lossless
(cherry picked from commit d51c8f4f5096badfd0beddd78646617643f6028f)
2026-09-27 18:38:58 +05:30
Adolanium
e46d4c0ade fix(compression): let the next summary build on a fallback handoff
A deterministic fallback summary replaced the older handoff in the transcript but never updated _previous_summary. The next compaction kept the stale in-memory summary and dropped the fallback row from its window, so the fallback's user asks, files and last dropped turns never reached the summarizer. Store the fallback body in _previous_summary the same way a normal summary is stored.

(cherry picked from commit 35417d2e1ffbb775c3eaff17b26623896afa56c1)
2026-09-27 18:20:40 +05:30
brooklyn!
9e7239acfd fix(desktop): pass wayland ozone on native Wayland sessions
Native Linux Wayland stayed on XWayland because the relaunch only ran for
WSLg. Append --ozone-platform=wayland when the user did not already choose
a platform. An explicit x11 hint and desktop.electron_flags still win.
2026-09-27 06:26:35 -05:00
John Paul Soliva
c9ccdb967d fix(desktop): the skip notice names the in-app update, and hermes desktop reopens the app it kept
Review follow-ups on the Windows skip:

- Most people who hit the boot loop launched Desktop from the Start menu and
  never open a terminal. The in-app update also rebuilds the app: the
  Windows shim waits for Desktop to exit, and the already-up-to-date path
  still completes with desktop=True. The notice and updating.md now name
  Update now in Settings -> About next to `hermes desktop`.
- cmd_gui took the skip's None as "no launchable app was found". A build
  that fails raises instead of returning None, so None there only means the
  skip, and `hermes desktop` now reopens the app that was kept.

Tests fold to the two invariants, each red when its half of the fix is
reverted: the stop spares its ancestor Desktop on every platform, and a
Windows packaged build under its own Desktop is skipped (now driven through
the real _desktop_ancestor_in with a fake process tree, instead of a stub).
2026-09-26 23:50:01 -04:00
John Paul Soliva
8f338a845a docs(update): the Windows Desktop rebuild is skipped when the update runs inside the app 2026-09-26 23:50:01 -04:00
Brooklyn Nicholson
8cb4fdc925 fix(process): heartbeats wake the agent only on new output, and never as a user bubble
A `terminal(background=true, heartbeat=N)` tick queued a notification every N seconds
whether or not the process had printed anything, and every queued event costs the owning
session a full model turn. On Desktop and the TUI that turn painted the wake as a user
bubble ("[Background process ... heartbeat #9 ... (no new output since the last
heartbeat)]") followed by the model's "Still running normally." — over and over, for a
process whose row on the status stack already said it was running — and while the wake
held the session's turn, the user's own prompt sat queued behind it.

- `ProcessRegistry._emit_heartbeat` skips a tick with no new output. The sequence counts
  delivered beats only; the "(no new output)" placeholder in the formatter is gone.
- TUI/Desktop type heartbeat rows `display_kind: hidden` (the kind both clients and the
  transcript preview already honour); the CLI paints a one-line receipt and persists the
  row hidden, so reopening the session in Desktop shows only the agent's reply.
- Desktop hydration drops heartbeat rows persisted by older backends the same way.
- `display.background_process_notifications: off` is honored by the TUI/Desktop poller and
  the CLI drain, not just the messaging gateway. `off` mutes process-driven wakes only:
  a finished `delegate_task(background=true)` still lands.

Supersedes #123123 (cherry-picked; scoped so `off` keeps subagent results) and #119202
(cherry-picked; `heartbeat: 0` is schema-valid so models that materialize every field
stop tripping the foreground guard).
2026-09-26 22:19:53 -05:00
Brooklyn Nicholson
33f45ca30b fix(gateway): keep one Windows gateway autostart mechanism
A successful Scheduled Task install returned without removing an
existing Startup-folder Hermes_Gateway.vbs or legacy .cmd, and the
fallback path wrote a Startup entry even while a task was still
registered. Both fire at logon, so the gateway launched twice.

install() now removes Startup entries after the task registers, the
fallback is skipped while a task exists, and reconcile_autostart_launchers()
converges an existing install to one mechanism.

Co-authored-by: David Metcalfe <80915+DavidMetcalfe@users.noreply.github.com>
2026-09-26 21:44:50 -05:00
Brooklyn Nicholson
a7c080ca66 feat(skills): brag and brag-slim join the optional-skills catalog as upstream stubs
latent-spaces/brag (MIT) turns the project you just built into a short
launch video with music, motion and share copy. It ships two skills:
/brag, the Hyperframes workflow with a bundled music and SFX library,
and /brag-slim, a single SKILL.md where the model builds the whole video
with local tools. /brag hands off to its bundled copy of brag-slim on
Claude Opus 5.5.

Both follow the impeccable/archify pattern: catalog stubs whose
metadata.hermes.upstream pointer makes
`hermes skills install official/creative/<name>` pull the live tree
through OptionalSkillSource._fetch_from_upstream. Nothing is vendored.

The brag stub documents that its Hyperframes path loads HeyGen's
hyperframes-* domain skills by name, and that the skills guard blocks
four of the five today (hyperframes-creative scores dangerous).
brag-slim has no such dependency.

Docs: two generated pages, two catalog rows, two sidebar lines.

Credit: Shunit Haviv Hakimi (shunithaviv), upstream author.
2026-09-26 21:41:31 -05:00
Hermes Agent
9d09662993 docs(desktop): describe what --ignore-existing skips 2026-09-26 17:15:10 -05:00
brooklyn!
de236c6a7f fix(desktop): ignore-existing skips discovered runtimes without repeating install
HERMES_DESKTOP_IGNORE_EXISTING=1 only wrapped the PATH probe, so a usable
active install or system Python still started a local serve. Skipping those
rungs on every resolve would make the post-bootstrap re-resolve return
bootstrap-needed and start the installer again. The first pass now falls
through to connect/onboarding; the re-resolve keeps the runtime just installed.

Fixes #117682
2026-09-26 17:15:10 -05:00
Brooklyn Nicholson
959c7649fd fix(updates/win): click-session poll, Intel-Mac installer docs, cua-driver opt-in autostart
click-session flake (#97982): a bare scrollIntoView() smooth scroll could be dropped under load, and the script
slept a fixed 3000ms before reading state. Extract click-session-helpers.mjs:
instant centered scroll, a bounded poll-for-composer loop instead of the
fixed sleep, and correct nested CDP envelope unwrapping (the old read logged
undefined).

Intel-Mac installer docs (#99033): the Hermes-Setup.dmg bootstrap installer
is built for Apple Silicon only, so Intel Macs hit "not supported on this
Mac". The desktop release pipeline already builds a native darwin-x64
bundle, so the docs now scope the arm64 limit to the bootstrap installer and
name the darwin-x64 bundle (or the CLI plus `hermes desktop`) as the Intel
path, in the desktop README and the platform-support build-targets section.

cua-driver autostart opt-in (#97389): Windows installs registered the
cua-driver-serve scheduled task on every install, with no opt-out, and
treated the task as an install-readiness requirement. Gate the
install-ready check and _repair_cua_driver_autostart_windows on the new
computer_use.autostart config key (default false = on-demand, fails
closed), extract the registration PowerShell into a testable helper, and
document the opt-in (EN + zh-Hans). Windows-only code path: unit tests
cover the registration args and the config gate; live Windows
verification pending.
2026-09-26 17:09:16 -05:00
kshitijk4poor
393f03dbcb docs(plugins): say dependency dirs are not carried across catalog updates
The catalog guide said every symlink among untracked files stops the update.
Since guard-excluded dirs (.venv/, venv/, node_modules/, tool caches) are now
pruned from the carry, links inside them never stop an update and those dirs
are rebuilt rather than copied. State that exception next to the symlink rule
(the _carry_user_files docstring was updated with the code change).
2026-09-27 01:42:52 +05:30
kshitijk4poor
03fae2fac8 fix(plugins): refuse symlinked user files on git-checkout updates
cff26600d2 stopped following symlinks when carrying untracked/ignored
files into a staged catalog update: a link planted after the installer's
scan could point outside the plugin root, past the guard. But it did so
by skipping them silently. Base followed the link and kept the content,
so a user whose ignored config.yaml is a link into their dotfiles now
loses that config on repin with no warning. _carry_user_files promises
to fail before publication rather than drop user state.

In a git checkout, symlinked files or dirs in the ??/!! set now fail the
update before publication, and the error names every such path. Links
are still never followed. Links under node_modules/ are .bin shims that
a reinstall recreates, so they stay skipped rather than blocking every
JS plugin's update. The no-git branch is unchanged: there, links may be
upstream's own.

The existing ignored-data-dir git test gains the case: the update
refuses, names data/link.yaml, and the live plugin keeps its revision,
its link and its data. This also gives the no-follow rule a test that
fails if the link is followed.
2026-09-27 01:42:52 +05:30
kshitijk4poor
1362b95c04 fix(plugins): keep dashboard/ and JS module files revision-owned on no-git carry
A subdirectory install has no .git, so the carry cannot tell removed
upstream code from user files and relies on a deny-list. That list missed
the dashboard surface (web_server_dashboard loads dashboard/manifest.json)
and .mjs/.cjs/.jsx/.tsx, so an upstream that dropped its dashboard or a
hook script got the old files back.

Add dashboard/ to _NO_GIT_REVISION_DIRS and the four extensions as a
carry-local set. They stay out of tools.plugin_guard.CODE_FILE_EXTENSIONS
on purpose: that set exempts code files from env-secret scan patterns, so
widening it there would weaken scanning rather than broaden it.

The docs now list what is actually enforced, and the existing subdir
update test pins dashboard/manifest.json + hooks/run.cjs removed upstream
are not resurrected.
2026-09-27 01:42:52 +05:30
JoaoMarcos44
d05b33cb4d fix(plugins): contain user-file carry within staged tree
(cherry picked from commit 97be7ac043a95f20268628e1a3c2e6d22c9c74d1)
2026-09-27 01:42:52 +05:30
JoaoMarcos44
23e678aaf9 test(plugins): cover destructive update paths
(cherry picked from commit 7d5cfb5cff73661fc96a0737c1b187fc4594c8d6)
2026-09-27 01:42:52 +05:30
kshitijk4poor
1b00d0ad7b docs(gateway): state the real bound on concurrent turn threads
The comment claimed turn concurrency is bounded at admission by
max_concurrent_sessions, but that defaults to unset and auto-resume is
uncapped. The real bound is one live turn per session plus turns
abandoned by the inactivity timeout, whose threads keep running. Say so,
and note in the max_concurrent_sessions docs that it is the only cap on
concurrent gateway turns.

Co-authored-by: Kyzcreig <9063726+Kyzcreig@users.noreply.github.com>
2026-09-27 01:05:06 +05:30
IntrepidBytes
8682d5791b fix(gateway): stop launchd osascript idle CPU spin
(cherry picked from commit ef6f22810341601bb8de58c9fbba5423361fbe23)
2026-09-27 01:04:39 +05:30
kshitijk4poor
c923645061 docs(env): OPENAI_API_KEY selects openai-api; OpenRouter only for sk-or- keys
The previous commit stops auto-routing a non-sk-or- OPENAI_API_KEY to
OpenRouter. The env-var reference still described OPENAI_API_KEY only as a
custom-endpoint key and the non-interactive setup hint listed it as an
OpenRouter alternative. Say what the key now selects so users with an
OpenRouter key in OPENAI_API_KEY know to move it to OPENROUTER_API_KEY.

Co-authored-by: notwitcheer <notwitcheer@users.noreply.github.com>
2026-09-27 00:56:11 +05:30
John Paul Soliva
7d49b46e15 fix(sessions): prune keeps the compressed-away start of a chat still in use
Retention prune (the default-on startup auto-prune, `hermes sessions prune`
and the dashboard prune) aged every session row on its own. A conversation
that rotating compression split into segments has an ended, old root by
construction, so once that root passed retention_days it was deleted while
the conversation's live tip was still being written: the pre-compression
turns vanished from the resume/Desktop history and from session_search, and
the tip was orphaned.

Prune now deletes a compression ancestor only together with every
continuation after it (`whole_lineages`), so a lineage ages through its
newest segment and goes as a unit once the whole conversation qualifies.
Branch, delegate, reset and tool children do not count as continuations.
The CLI and dashboard prune previews pass the same flag, so they list what
prune deletes; bulk export keeps its current selection.

(cherry picked from commit b082fc6ffa02607cba219a5dfa361d4f3bf4c266)
2026-09-27 00:43:39 +05:30
kshitijk4poor
3b5945499e docs(sessions): a resumed memory-only row becomes repairable; the prompt is not healed 2026-09-27 00:40:21 +05:30
kshitijk4poor
e4154b88dd docs(sessions): memory-only repair-prompts rows self-heal on resume
They are not "never auto-repaired": resuming such a session re-pins the
full tool surface (restore_agent_tool_prefix), after which a scan sees
skill_manage without the Skill Safety guidance and clears the prompt.
2026-09-27 00:40:21 +05:30
kshitijk4poor
0c1a1036fc refactor(sessions): drop the repair-prompts pin clear; single-pass scan
A memory-only tools[] pin self-heals: restore_agent_tool_prefix appends every
fresh tool to the pin and persists it (merged != pinned) on the next turn. The
clear_pin flag was also unreachable in scan mode (findings need skill_manage).
So drop clear_pin/_HYGIENE_PIN_TOOLS and clear_system_prompt_for_rebuild, and
reuse update_system_prompt(sid, None), which already nulls prompt+hash and GCs
in one write.

The detector now parses each pin and checks the marker once per row, keyed on
prompt_builder.SKILL_SAFETY_HEADING instead of a hand-copied literal. The scan
classifies each compact_rows page as it arrives, keeps only finding dicts, and
dedupes ids that OFFSET paging can re-serve during concurrent inserts.
2026-09-27 00:40:21 +05:30
kshitijk4poor
8918e8a0fa fix(sessions): only auto-repair prompts with skill_manage evidence
The repair-prompts detector cleared two legitimate prompts:

- a pin with skills_list/skill_view but no skill_manage and zero skills
  installed: build_skills_system_prompt returns '' and SKILLS_GUIDANCE is
  only emitted with skill_manage, so the healthy prompt has neither marker;
- an exact memory-only pin, which is also a user toolsets=[memory] config;
  the healthy rebuild was re-flagged on every run (not idempotent).

Key the decision on the missing '## Skill Safety' guidance, which is
unconditional when skill_manage is in the pin, and require skill_manage in
the pin. Memory-only rows are now reported as unverifiable; the explicit
SESSION_ID override still clears them and their pin.

Docs: describe the tightened rule and note that a running gateway keeps
cached prompts in memory, so it must be restarted after --apply.
2026-09-27 00:40:21 +05:30
JoaoMarcos44
b09ed52c84 fix(sessions): make prompt repair atomic
(cherry picked from commit 55bcd8f6ccd667a5352769ad82bcbad1e73f82b1)
2026-09-27 00:40:21 +05:30
JoaoMarcos44
7b3dd14768 fix(sessions): make prompt repair evidence-safe
(cherry picked from commit b1eaaeabbeeb3500de390b77de62763bf33c86ae)
2026-09-27 00:40:21 +05:30
kshitijk4poor
661a22ae5c docs(profiles): a skill dropped from an owned category stays on update
Merging an owned category per skill means a skill the author removes from
the distribution is no longer deleted on update, matching top-level
skills/. Say so in the English and zh-Hans reference so authors are not
surprised that retired skills linger.
2026-09-27 00:38:57 +05:30
John Paul Soliva
b4c0052b0d docs(profiles): an owned skills directory or category merges per skill
(cherry picked from commit b3f2e55d8a3cc8bb7de07a65a8f40e98d23c1776)
2026-09-27 00:38:57 +05:30
teknium1
63e44332f5 fix(kanban): a worker the dispatcher never recorded registers itself instead of being run twice
A dispatcher SIGKILLed between _call_spawn_fn and _set_worker_pid leaves a
live worker on a run with worker_pid NULL. release_stale_claims only extends
an expired claim for a recorded live pid, so on TTL expiry it reclaimed the
card and spawned a second worker beside the first: double billing, double
side effects, and a board showing one clean completed run (the first
worker's kanban_complete is refused as stale). Main CI hit it in
test_dispatcher_sigkill_mid_tick_never_destroys_or_duplicates_cards.

The worker now records its own pid on its run before the first model call
(adopt_worker_pid, worker_registered event, host-local claims only) and
exits without working the card when its run was already reclaimed. The
reclaim UPDATE also compares worker_pid so a registration landing between
the stale-claim SELECT and the UPDATE keeps the claim.

Repro: temporary sleep between spawn and pid record + kill 0.2 s after the
spawned event + slow first model reply -> 4/4 red on main with the CI
signature, 8/8 green here.

Fixes #121556
2026-09-26 11:16:45 -07:00
MongLong0214
bb17b1f74c fix(compression): keep the pending round's images when it fits
Compaction spared a pending tool round's text results but not its
images. The image-retirement pass kept only the newest three image
results across the transcript, so four parallel vision calls in one
unread round lost the oldest image before the model saw any of them.
On a single-prompt run the final media pass lost three of the four:
compaction re-appends the task as a user row after the round, and the
pending round was looked up after that row was added, so nothing was
spared.

Both image passes now skip a pending round that fits the hard share,
and the pending round is found before the task row is re-appended.
Completed rounds and a pending round over the hard share keep the
existing policy.

(cherry picked from commit eb7661f4365f009d5f9ef85e26f2f4b6ac83632e)
2026-09-26 23:45:36 +05:30
MongLong0214
65f185c594 fix(compression): keep the pending tool round when two steers follow it
The pending-round check skipped only one trailing /steer row. Two can
land in the same iteration: one is appended when the tool batch ends,
and a steer sent after that is drained before the next request and
inserted right after the newest tool result. The transcript then ends
tool -> steer -> steer, so the check stopped on the second steer row,
found no pending round, and preflight compaction replaced the unread
tool output with the one-line pressure stub.

The check now skips every contiguous trailing steer row, identified the
same way as before, and still stops on any other user row. The existing
regression gains a case with two steer rows after the round.

(cherry picked from commit 455a1a3868b3843d15401781363ce796bae96b5b)
2026-09-26 23:45:36 +05:30
MongLong0214
18eb09f5db fix(compression): keep the unanswered tool round verbatim through mid-turn compaction
Preflight compaction can fire right after a tool round, before the model
has read its results. The protected-tail passes then treated that round
like any old output: pass 2 and the pressure pass (#61932) replaced its
results with one-line stubs, and when the newest row alone exceeded the
tail ceiling the cut landed at the end of the transcript and summarised
the round with its whole turn. The model then re-ran the call, side
effects included, or answered without the output.

_pending_tool_round finds the tool results the transcript ends with,
skipping one trailing /steer row (a steer is delivered after the newest
result, before the next API call). Both passes spare that round, and the
tail cut aligns from the row before the end so the group stays whole.
The one exception is a round that alone exceeds the tail's hard share
(20%) of the input budget, the window minus the output reservation: it
still gives way, as #61932 requires. _effective_input_window is
extracted from _compute_threshold_tokens so both use the same budget;
thresholds are unchanged.

(cherry picked from commit 9d74e22379cd7dc39636c522175772d0165db4ac)
2026-09-26 23:45:36 +05:30
kshitijk4poor
f077152871 docs(cron): say the counter is reset to a valid count, not set to one 2026-09-26 23:00:54 +05:30
kshitijk4poor
7ca5cca50a fix(cron): an Infinity or negative repeat.completed no longer breaks load_jobs
06a495cc5b normalized non-int counters but caught only TypeError/ValueError.
json.loads turns a hand-edited Infinity / -Infinity / 1e999 into float inf,
int(inf) raises OverflowError, and that escaped load_jobs, so every job
(list, tick, mark_job_run, hermes cron list) failed, not just the bad one.
Catch OverflowError (-> 0), and also clamp a negative int count, which
granted extra runs. The docs sentence now says non-negative.
2026-09-26 23:00:54 +05:30
Yuan Li
a5e8ad9f02 fix(agent): bound sustained summary-overload aborts so they cannot guarantee a session wipe
One overload abort preserves the transcript so a retry can still win (#115906).
But when every summary attempt keeps aborting under a sustained outage the
transcript only grows until the session exits compression_exhausted, which the
gateway answers with an auto-reset discarding the ENTIRE transcript — bounded
middle-window loss becomes total session loss, deferred.

After 3 consecutive overload aborts in one session the overload stops counting
as a terminal summary failure and compress() commits the deterministic fallback
(failure_class=summary_overload_degraded) — the same bounded degrade the
repeated-stall ladder already takes (#112420). A successful summary resets the
budget; abort_on_summary_failure=true still hard-aborts every attempt.

Fixes #123167

(cherry picked from commit 2941aadffa71a3623aee26dfb1109106b0555741)
2026-09-26 22:42:38 +05:30
kshitijk4poor
66c5099b31 docs(sessions): note same-id recreate when deleting a live session
A session deleted while its chat is still running is recreated under the
same id with the full in-memory transcript on the next save (the gateway
session-key mapping expects the id to be stable). Say so next to
`hermes sessions delete`, in English and the zh-Hans mirror.

Fixes #123583

Co-authored-by: 赵桂雄 <daniel21436@hotmail.com>
2026-09-26 22:39:42 +05:30
kshitijk4poor
06a495cc5b fix(cron): normalize any non-int repeat.completed, not only null
A hand-edited "completed": "2" still crashed every recorded run ("2" + 1),
and 1.0 was stored as 2.0 ("2.0/3"). load_jobs now coerces any non-int
counter to a non-negative int (0 when unparseable). Document the load-time
repair next to the direct-edit tip.

Co-authored-by: John Paul Soliva <soliva.johnpaul@icloud.com>
2026-09-26 22:04:17 +05:30
kshitijk4poor
11c50f05d0 refactor(compression): Astra native-compaction gate reuses is_astra_model
The -900k alias fix hand-rolled a second copy of the Astra slug set and its
vendor-prefix normalization. agent/reasoning_effort.py::is_astra_model is the
documented single home for that set (picker, effort vocabulary and request
sanitizer already key off it), so the gate now calls it and a future Astra
alias stays a one-line edit. The gpt-5.6 marker check is back to main's exact
form.

Tests move into the existing parametrized Astra gate table, which checks both
the capability resolver and the per-request gate: -900k on official Codex OAuth
is eligible; -900k through a relay or on provider openai is not. Docs and the
config example no longer say "exact gpt-6-astra".
2026-09-26 19:27:41 +05:30
kshitijk4poor
b17e037e35 fix(slack): a follow-up in a flat reply_in_thread: false channel keeps the silence fallback
With reply_in_thread: false the whole channel is one session and the bot
answers top-level, so an unmentioned top-level message there is a
follow-up in a conversation the bot is part of, like a thread reply.
reply_expected is now False for a free-channel message only when it starts
its own session (a new top-level thread), else None. The bot-id set is
built inside _slack_reply_expected, as _channel_gate_allows does.
2026-09-26 07:21:49 +05:30
kshitijk4poor
6c566fcd6d fix(slack): thread follow-ups and reaction triggers keep the silence-marker fallback
The Slack rule marked every admitted message that was not a DM, a mention
or a command as not addressed, so a plain "done?" in a thread the bot is part
of, or a reaction trigger, could end on a bare silence marker and vanish,
the case #111624 fixed (#110952).

reply_expected is now False only for a message that opens by @mentioning
someone else, or a top-level message a free-response channel admitted
without a mention. Reaction triggers and pipe-form self mentions count as
addressed; other thread replies are None (visible fallback). The
free-channel predicate moves into _slack_is_free_channel so the gate and
the rule read the same one. The test drives the real _handle_slack_message.
Docs describe the rule in its own note instead of the
ignore_other_user_mentions tip, and the messaging index documents the
human-turn fallback.
2026-09-26 07:21:49 +05:30
Victor Kyriazakos
b08bb5afb8 fix(gateway): a bare silence marker on a turn not addressed to the bot stays silent
Since 5ea8fb2b78 (#111624, for #110952) the gateway rejects a bare silence
marker on any human turn and delivers "The model returned only a silence
marker for a message that needed a reply" instead. That protects a human
who asked this bot something and got nothing back. It also fires on every
human message the adapter admitted without the bot being addressed at all:
a free-response channel, a thread follow-up under
`thread_require_mention: false`, or a message @-mentioning another person
or bot with `ignore_other_user_mentions: false`. A bot whose SOUL declines
peer-addressed turns with a deliberate marker now posts that notice on
every such message. A fleet running several bots in shared Slack threads
reported it as spam on v2026.9.21. #37940 established that intentional
silence must not be re-inflated. Both contracts hold once the turn knows
whether a reply was expected.

`MessageEvent.reply_expected` (True, False, None) is set by the adapter
where the message is admitted. Slack (`slack_reply_expected`): a 1:1 DM,
an @mention of this bot or a command is True, anything else it admits is
False. Other adapters leave None, which keeps today's behaviour, so nothing
changes for them until they are ported. `response_filters.silence_allowed`
holds the one rule (machinery turn, or reply not expected) and both call
sites use it: the live turn in `run_turn._hmwa_shape_agent_response` and
the crash-recovery redelivery from #120377 (1136f135dd), which reads the
flag back from the persisted turn metadata. The suppressed case logs one
DEBUG line naming platform and chat.

Operator workaround until this lands: `platforms.slack.extra.
ignore_other_user_mentions: true` drops peer-addressed messages before a
turn exists.

(cherry picked from commit 094439776ab898cccde303a1c2c911c8ab5bfb75)
2026-09-26 07:21:49 +05:30
David Metcalfe
27263a2768 docs(skins): document the customCSS skin field and 32 KiB cap 2026-09-25 20:00:57 -05:00
brooklyn!
e47db9414a feat(catalog): restore website skill links and native browse guidance 2026-09-25 12:09:55 -05:00
ethernet
b5d583c4ac feat(release): start the gates and the signed candidates together
Every gate and every candidate now needs only admit, so the signed macOS
and Windows builds and the docker image stop waiting behind the full CI
run. acceptance stays the one join: it still needs ci, docker and all six
candidates, so what can be promoted is unchanged.

The four gates that skipped under --skip-tests only because ci skipped
(nix, termux-checks, windows-live, install-e2e) and pm-bundle needed their
own condition. SKIPPED_BY requires the gate to observe 'skipped', so
dropping the edge alone would have left them running and blocked the
release instead of failing it.
2026-09-25 12:50:53 -04:00