17 Commits
Author SHA1 Message Date
unsupportedpastels 342a55ebad fix(cli): /discard usage hint, parity note and connection test fakes
Addresses maintainer review on #143595.

Refs #143547
2026-10-25 11:30:24 -04:00
kshitijk4poor 9ca23c6840 fix(cli): run every /cron form on the gateway that owns the cron store
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
2026-10-20 13:08:24 +05:30
Teknium 71b45e13aa docs(gateway): the parity note for /save names Desktop's client-side write
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.
2026-10-18 20:50:00 -07:00
Teknium 7fb93521a6 refactor(acp): one route table serves editor commands and documents them
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
2026-10-16 21:13:47 -07:00
Teknium 69f80031de fix(acp): registry commands the editor path does not serve get a named reply, not Internal error
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
2026-10-16 21:13:14 -07:00
Teknium 88d56fcd87 fix(gateway): /skills review verbs run the messaging handler for a local session
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.
2026-10-13 02:51:42 -07:00
Teknium 0d93ab57fa docs(gateway): the parity table's Classic CLI column is main's HermesCLI
`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.
2026-10-13 02:51:03 -07:00
Teknium d3cd685fb6 fix(gateway): per-session setting commands run their messaging handler for a local session
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)
2026-10-13 02:43:13 -07:00
Teknium 88056a7556 docs(gateway): parity table rows for the admitted authoring and goal-family commands
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).
2026-10-13 01:58:38 -07:00
Teknium 2efe028116 fix(tui,gateway): /status, /context and /cron read as main's TUI on the shared owner
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.
2026-10-13 00:35:44 -07:00
Teknium bec207e9e4 docs(parity): a refused Ink subcommand keeps the rest of the command routed
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)
2026-10-10 22:49:21 -07:00
Teknium 37434ad0e3 fix(tui): display preferences say they are not saved on the shared gateway
/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)
2026-10-10 22:49:20 -07:00
Teknium 1d691d84d8 fix(tui): Ink /rollback refuses on the shared gateway; parity table says so
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)
2026-10-10 22:49:20 -07:00
Teknium 9a13c5e5e1 fix(tui): refuse shared-gateway setting writes by name instead of a bare invalid_params
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
2026-10-10 05:52:03 -07:00
Teknium 8a444a32ac fix(tui): keep process-global sidecar verbs off the shared owner; scope Ink /stop to its session
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
2026-10-10 05:52:02 -07:00
Teknium 7640528eae fix(cli): keep GatewayChatView.run under its complexity cap; BOM-tolerant parity reads
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)
2026-10-09 02:10:48 -07:00
Teknium 60302f0054 docs(gateway): generated command-parity table for local sessions, with a drift test
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)
2026-10-09 02:10:48 -07:00