Symptom: in a classic `hermes chat` session on the shared gateway, /cron
add|edit|pause|resume|run|remove still ran main's handler in the CLI
process, which called the cronjob tool there and wrote the profile's
cron/jobs.json from the client. Under strace, every write to jobs.json
came from the `chat --cli` process and none from the gateway.
Root cause: the one-gateway runtime (#106742) made the gateway the
owner of the profile's runtime state and of the ticker that fires cron
jobs. It left CLICommandsMixin._cron_* in the client, and the
gateway's slash.exec refused /cron for every local session except TUI
reads (2efe028116). That left the cron store with two writers.
Change:
- Move main's /cron handler (overview, list and every write) into
hermes_cli/cli_cron_lines.py as line builders (cron_command_lines),
with no other behaviour change. The CLI prints them when it has no
gateway session. The unused CLICommandsMixin._cron_list goes; its
test and eval read cron_list_lines directly.
- On the gateway, /cron is an owner command
(gateway/session_cron_commands.py). Bare /cron and `list` are reads
for every local session; that includes Desktop- and ACP-created
sessions, which main refused with unsupported_command. Every other
form is an admission-fenced control. An unclosed quote is refused as
invalid_params before pre_command fires. `/cron list -a` keeps the
list-all alias that the TUI read path accepted.
- A /cron write runs only when it arrives on a connection that is not
attached to the session: the classic CLI's own command socket
(GatewayConnection.call_until), which waits with no deadline. A
connection attached to the session keeps main's unsupported_command.
That is how Ink, Desktop and ACP send /cron: on their main
connection, which the gateway answers one request at a time and
which waits with a deadline, while a /cron run lasts as long as its
job. The gate is on the requesting connection, not on the surface
that created the session, so a classic CLI that resumed a TUI-made
session still writes (as on main), and Ink on a CLI-made session is
still refused.
- The classic CLI sends /cron to slash.exec
(gateway_setting_commands.GATEWAY_SETTINGS) with no deadline, on a
socket of its own (gateway_plugin_commands.call_unbounded, now shared
with plugin commands; its Ctrl+C handle is renamed
_gateway_command_wait). Ctrl+C prints the stopped line and
propagates.
- Regenerate the parity table. test_pre_command_in_gateway pinned the
old refusal of every classic CLI /cron; it now pins that the accepted
/cron forms fire pre_command once and a refused one (an unclosed
quote) fires nothing.
Why: the design contract allows one writer per owned store, and
clients only read and submit. The gateway already serves this
profile's cron store to its ticker and to Desktop (cron.manage). Now
the classic CLI's writes go through the same process.
Refs #134889 (T1)
Refs #139158
The generator's /save note still said the export file is written on the gateway host; Desktop with
a current gateway saves session.export text through its own dialog. Fix the note in
scripts/gen_gateway_command_parity.py and regenerate the table.
The parity generator kept its own ACP_ROUTES list beside the membership
checks in run_command, so an ACP route could change while the documented
cell (and its drift check) stayed the same. acp_adapter/gateway_commands.py
now holds ROUTES (command -> documented cell, handler): run_command
dispatches through it, anything else is the owner's slash.exec, and the
generator reads the same table. The rendered parity doc is unchanged.
Review finding on #140190 (Greptile)
Refs #134889 (ACP-17)
Refs #140138
On the gateway ACP path every registered Hermes command outside the adapter's
small allowlist (/title /yolo /retry /undo /skills /clear /bg ...) raised
GatewayClientError('unsupported_command'), which the ACP SDK turned into a
JSON-RPC -32603 "Internal error" for the whole prompt, while /help said
unrecognized commands go to the model.
Map them the way the classic transport client (gateway_chat_commands.py) does:
/title <name> is session.mutate rename, /undo [N] and /retry the rewind
mutation, /clear the reset, and every other registry command goes to the
owner's slash.exec, which runs what it serves for a local session (/yolo,
/skills, bare /title, /plan's turn...) and refuses the rest. That refusal
(and /branch's pointer to session/fork) is the command's reply with
stop_reason end_turn, using the shared "not available on the shared gateway
yet" wording. The parity table's ACP column now reads those routes.
Red on main: tests/acp_adapter/test_gateway_registry_commands.py (real
acp_adapter.entry stdio process + real gateway.run daemon) failed with
{'code': -32603, 'message': 'Internal error', 'data': {'details': 'unsupported_command'}}
on /title.
Refs #134889 (ACP-17)
Refs #140138
Desktop exposes exactly the write-approval review slice (desktop_subcommands
pending/approve/reject/diff/approval) and sent it as slash.exec, which the
owner refused; Ink's bare/pending forms hit the same refusal. The gateway's
own _handle_skills_command already serves that slice (the hub stays
CLI-only and answers its "Search/install are CLI-only" line), and its
`approval on|off` writes skills.write_approval to the profile's config.yaml,
which is where main's CLI wrote it too. Admitted: bare/pending/diff as reads,
approve/reject/approval as controls; anything else (install, search, ...)
stays unsupported_command.
Proof: test_session_read_commands runs /skills pending, /skills reject and the
refused /skills install on a real daemon; parity table regenerated.
`hermes chat` is main's HermesCLI with a gateway-backed agent and session
store (hermes_cli/gateway_agent.py, gateway_session_store.py); the column
still described the retired thin client (gateway_chat_commands.VIEW_ROUTES),
which now only fronts --stream-json and the safe-mode bypass. The generator
reads HermesCLI._slash_handler for every registry command and names the main
handler it runs in-process; gateway-only commands read "messaging-only:
unknown command". The header says which client is which, and the
/approve /deny control verdict cites its test. Table regenerated.
slash.exec admits /reasoning /voice /approvals /codex-runtime /footer /busy /verbose /yolo /fast
/personality and runs the same _handle_<name>_command a messaging chat runs. Only what made that
handler wrong for a LOCAL session changes (gateway/session_setting_commands.py finds its LiveSession):
- /reasoning <level>: the session override now reaches a local turn (current_reasoning_config honours
it before the frozen policy), config.get reasoning and session.info; `--global` saves AND applies to
this frozen session; show/hide sets the session's streamed reasoning; a session.mutate reasoning
pick clears a typed override.
- /fast: the session's tier is read from its policy; `--global` applies to this session too.
- /busy: applied to the session (config.get busy) besides the saved default.
- /verbose: no messaging opt-in gate for a local session; cycles its own tool-progress mode
(off/new/all/verbose, no messaging-only `log`).
- /footer: parsed its argument from a field MessageEvent does not have, so `status|on|off` toggled.
Proof: tests/gateway/test_session_setting_commands.py runs every handler on a real local session
against a loopback model, reads back through config.get and the next turn's request
(reasoning_effort high, then low after --global).
(cherry picked from commit 64e27e54954b5c5a80ca781d7fabcc7b8ea78df7)
Regenerated by scripts/gen_gateway_command_parity.py: /plan /learn /init /initiate-setup /moa
/blueprint /egress /goal /subgoal /loop /heartbeat /review /refine /debug /curator /login are
``control`` on slash.exec, so the classic CLI, Ink and Desktop columns reach them. The stale
"/curator has no gateway handler" note is gone (``_handle_curator_command`` exists now).
A TUI session's slash.exec reached the messaging gateway's handlers:
/status printed "📊 **Hermes Gateway Status**" markdown, /context the
gateway's gauge instead of the category table, and /cron was refused
(FINDINGS rows 17 / 19 / 21).
For a session created with source 'tui' the owner now renders main's
TUI text itself (gateway/session_tui_reads.py), inside the owning
profile's scope and the session's workspace:
- /status: main's session.status lines (status_report.build_status_fields)
from the stored row and the live agent;
- /context: main's _format_live_context_output, the conversation counts,
then render_context_breakdown_lines(grid=False) off the live agent;
- bare /cron and /cron list [--all]: the classic CLI's text, now built by
cron_overview_lines / cron_list_lines in cli_commands_mixin (the CLI
prints the same lines). Every /cron write stays refused.
Other surfaces keep the gateway handlers. The parity table records the
TUI reads.
Live (tmux, fake provider): all three pages match MAIN line for line,
apart from the session id format.
The parity generator treated any canonical.controls.notAvailable in an Ink
command block as "the whole command refused". After the /voice tts refusal it
would have reported all of /voice as refused, although /voice on|off|status
still reach voice.toggle. ink_cell now recognises a refusal that names a
subcommand ('voice tts') and reports the route plus "`tts` refused". The
table is regenerated; /voice is the only row that changed.
Test: tests/hermes_cli/test_gateway_command_parity_live_session.py::test_a_refused_subcommand_keeps_the_rest_of_the_command_routed
(cherry picked from commit 08098c50613c90f51f9e36353d4a24ae55beb4d6)
/mouse, /density, /details (global and per-section), /focus, /statusbar and
/battery patched the view, printed success and sent config.set with
.catch(() => {}). The shared gateway's config.set accepts only
busy/verbose/yolo/model (gateway/session_busy_controls.py::busy_config) and
answers invalid_params for the rest, so the preference silently vanished on
the next launch while the notice implied it had been saved.
These are client display preferences, not session state, so they still apply
to the view; on gw.isCanonical the command no longer sends the doomed write and
adds "/<cmd> applies to this view only; not saved on the shared gateway"
(canonical.controls.prefNotSaved, translated in all 16 bundled TUI packs) after
its own notice. A standalone backend persists through config.set as before.
The parity generator marks such Ink cells "(preference not saved)" instead of
listing config.set as the route; the table is regenerated.
Test (red on r11-base): ui-tui/src/__tests__/canonicalDisplayPrefs.test.ts
Review: andrexibiza 28
Review: andrexibiza 5479692359-P3.c
Review: kshitijk4poor P3-tui-config-set-keys
(cherry picked from commit 1671c2c8f1be9593bd0f216b8fb16ae8633742bf)
Ink sent rollback.list/diff/restore with the shared-owner session id. The
owner has no handler (-32601), the interactive fallback reaches the legacy
sidecar, and those handlers are @_rpc(..., live_session=True): they resolve
the id in the sidecar's in-memory _sessions map, which never holds an
authority session, so every /rollback answered "session not found". The
parity table still listed Ink as "sidecar: rollback.*" because _rpc_cell only
flagged session-namespace methods.
Ink /rollback now refuses on gw.isCanonical with canonical.controls.notAvailable
(as /reload and /reload-mcp do) until the owner serves checkpoints; checkpoint
restore is not routed through the owner's cwd. The parity generator reads the
live_session=True sidecar RPCs from tui_gateway and reports a client cell that
depends on one as broken (4001) instead of served; the table is regenerated.
Tests (red on r11-base): ui-tui/src/__tests__/canonicalRollbackRefusal.test.ts,
tests/hermes_cli/test_gateway_command_parity_live_session.py
Review: dokterdok N39
(cherry picked from commit 8cc6968010a8f8f66dc468664bcb5d7fe5b7f90c)
On the shared owner the canonical `config.set` accepts only busy / verbose / yolo / model, so
Ink `/theme <v>`, `/skin <v>`, `/indicator <v>`, `/reasoning <v>`, `/fast <v>` and
`/personality <v>` each answered a bare `invalid_params`, while the generated parity table
listed them as working (`config.get, config.set`). Premise probe on a real authority: every
read answers (`theme auto`, `skin default`, `reasoning medium`...), every write
`{code: 4001, invalid_params, fields: [key]}`.
These are config.yaml / per-session settings with no frozen-policy owner verb yet (the parity
table's "port: per-session setting"), so the fix is the honest refusal the other canonical
commands already give: the bare form still reads the current value; a change prints
"/x is not available on the shared gateway yet" and sends nothing. The parity generator names
a refused client block as such (its served `.get` read kept) instead of "config.set".
Test: canonicalSlashCommands "setting writes ..." red on r10-base session.ts (`config.set`
sent), green after.
Review: https://github.com/NousResearch/hermes-agent/pull/106742#pullrequestreview-5475590057
Ink /stop sent a sessionless `process.stop`; the authority answered -32601 and the legacy
fallback ran `process_registry.kill_all(source="process.stop")` on the shared owner, killing
every chat's, Desktop window's and served profile's background processes, persisted jobs
included. `/agents pause` (global spawn gate), `/reload-mcp` (every MCP server), `/reload`
(rewrites os.environ from the launch .env) and `agents.list` (every chat's commands) reached
the same process-global handlers.
- tui_gateway/ws_legacy_fallback.py: `process.stop`, `delegation.pause`, `reload.mcp`,
`reload.env` and `agents.list` never fall back from an authority connection; the client keeps
the authority's answer. `process.kill` keeps its fallback: that handler is session-scoped.
- gateway/session_ancillary.py: the owner serves `process.stop` itself under session:control,
stopping exactly the processes `process.list` projects for the session (routing key + owner
among session/agent/subagents), persisted ones of THIS session included (explicit stop).
Managed workers answer unsupported_projection like the reads.
- Ink: /stop sends its session id and, when a turn is running on the shared gateway, interrupts
it first (registry busy policy interrupt_then_dispatch, as classic and Desktop do);
/agents pause|resume, the overlay's pause toggle, /reload-mcp and /reload say "not available
on the shared gateway yet" instead of a version-skew error.
- The parity generator reports a client-side canonical refusal as refused (it called /bg, /btw,
/save "canonical route"); table and gateway contracts regenerated.
Test: tests/gateway/test_session_process_scope.py drives a real authority over an authenticated
WS with four real child processes: red on r10-base ({'killed': 4}, all exited), green after.
Review: https://github.com/NousResearch/hermes-agent/pull/106742#pullrequestreview-5475590057
The resume footer branch pushed run() to CC 21 (cap 20): the finite exit ids move into
_print_exit_ids. The parity generator reads sources with utf-8-sig (Windows BOM policy).
Review: https://github.com/NousResearch/hermes-agent/pull/106742#issuecomment-6073624820
(cherry picked from commit c144eefc4c09ea5dd878022a796d1f0bc18fe394)
Reviewers asked which commands each local client can run on the shared gateway; the only
answer was reading five code paths. scripts/gen_gateway_command_parity.py walks
COMMAND_REGISTRY (103 commands) and records, from the code itself:
- the gateway's slash.exec verdict for a local session (gateway/session_commands.py
command_verdict: read / control / refused, with the read-only argument forms)
- the classic CLI view's route (hermes_cli/gateway_chat_commands.py VIEW_ROUTES)
- Ink's handler (canonical route / local / RPCs; a session-namespace RPC the authority does not
serve is marked broken, other unserved RPCs reach the legacy sidecar)
- Desktop's surface (local spec table + the registry's desktop= dump; rpc specs fall back to
slash.exec on -32601)
- ACP (slash_mutation)
- a note: toggles are "port: per-session setting", plus why handled-but-not-read commands
(/agents /platform /sessions /debug /curator ...) are not local reads
website/docs/developer-guide/gateway-command-parity.md is the output (linked from the classic
CLI refusal line); tests/hermes_cli/test_gateway_command_parity.py fails when any verdict changes
without regenerating it. The classic CLI doc now lists the commands and flags this round added.
Review: https://github.com/NousResearch/hermes-agent/pull/106742#issuecomment-6073624820
(cherry picked from commit 2005bfc30683326b1216eebd2fed2d7c7768e630)