Skip to content

fix: intercept /sessions and /resume slash commands in WebUI (#6224) - #2

Closed
webtecnica wants to merge 1 commit into
masterfrom
fix/6224-sessions-resume-slash-commands
Closed

fix: intercept /sessions and /resume slash commands in WebUI (#6224)#2
webtecnica wants to merge 1 commit into
masterfrom
fix/6224-sessions-resume-slash-commands

Conversation

@webtecnica

Copy link
Copy Markdown
Owner

Problem

When typing /sessions or /resume in the WebUI composer, the autocomplete popup shows them as valid commands (they're exposed by /api/commands), but they get sent as plain text to the agent instead of executing. This is because the agent command registry exposes both as non-cli_only commands, but the WebUI send-time dispatch in static/messages.js had no branch to catch them.

Fix

Add a native-intercept branch in the _parsedCmd && !_cmd block of send() (alongside the /pet special-case at ~line 1451). When the user types /sessions or /resume:

  1. Expand the sidebar (to show the session browser)
  2. Refresh the session list via renderSessionList()
  3. Clear the composer

This follows the same guard pattern used by other branches in this block, with typeof checks before calling any function.

Root cause analysis (per maintainer)

  • api/commands.py:96-108 list_commands() emits every registry entry not marked gateway_only or in _NEVER_EXPOSE
  • sessions and resume are registered without cli_only, so they pass the filter
  • _AGENT_COMMANDS_RUN_ON_WEBUI (line 1136) contains reload-mcp, reload-skills, codex-runtime, credits — but NOT sessions or resume
  • None of the branches in the dispatch match, so the block falls through to the normal send path

Alternative considered

Marking them cli_only=True on the agent side would make the existing cli_only branch fire, but that's worse UX given the WebUI genuinely supports session browsing/resuming.

Closes nesquena#6224

…a#6224)

Add a native-intercept branch in the  block of
send() alongside the /pet special-case. When the user types /sessions
or /resume, expand the sidebar and refresh the session list instead of
sending the raw slash text to the agent.

Root cause: the agent command registry exposes sessions/resume as
non-CLI-only commands, so the autocomplete popup shows them, but the
WebUI send-time dispatch had no branch to catch them, causing the
literal text to be sent as a prompt.
webtecnica pushed a commit that referenced this pull request Jul 18, 2026
…esquena#6243)

* fix(reasoning): gate reasoning_effort ladder to GLM-5.2+ on native zai

Z.AI's API (docs.z.ai) defines reasoning_effort as GLM-5.2+ exclusive, but
hemes-webui advertised the full 6-level ladder for all 7 GLM models because
_candidate_supports_reasoning has an unconditional glm token match and
_filter_reasoning_efforts_for_provider had no ZAI branch. Six of seven catalog
models (glm-5.1, glm-5, glm-5-turbo, glm-4.7, glm-4.5, glm-4.5-flash) showed a
selector whose values the endpoint silently ignores, and GLM-4.7 (forced thinking
that cannot be disabled per Z.AI docs) showed a 'none' option with no effect.

Add a ZAI branch to _filter_reasoning_efforts_for_provider mirroring the existing
OpenAI/Gemini/Anthropic ceiling pattern: strip the whole ladder for pre-5.2 GLM
models and for the forced-thinking GLM-4.7 family; preserve the full ladder for
GLM-5.2+ (whose accepted values match VALID_REASONING_EFFORTS exactly). The gate
is scoped to the native zai provider only (aliases glm/z-ai/z.ai/zhipu all
resolve to zai); aggregator providers are untouched because they route through
their own routers, not Z.AI's native endpoint.

The glm family-detection heuristic in _candidate_supports_reasoning is unchanged
— GLM models DO support the thinking on/off toggle at the family level; this fix
is specifically about the reasoning_effort intensity ladder.

State layer: agent.reasoning_effort config + UI dropdown options derived from
resolve_model_reasoning_efforts. Invariant: UI options and coercion now agree
and match Z.AI's per-model docs (max offered only for GLM-5.2+, none never
offered for forced-thinking models). Out of scope: the thinking:{type:...}
request-field translation lives in the external agent/gateway layer.

* fix(reasoning): close ZAI coercion gap + harden test assertions

Address Greptile review on nesquena#6219 (round 1):

1. Vacuous test assertion (test_glm_5_2_preserves_none_sentinel): the 'or'
   fallback made the assertion always-true, so a regression stripping 'none'
   for GLM-5.2 would go undetected. Rewrote to inject 'none' via the raw source
   (mocking _resolve_model_reasoning_efforts_impl) and assert it survives — the
   test now genuinely exercises the preservation branch.

2. Coercion gap for non-max stored levels on pre-5.2 GLM: the existing
   'if ceiling and raw not in ceiling' guard treats an empty ceiling as 'no
   rule' (preserving the configured effort verbatim per nesquena#3505), so a stored
   'high'/'medium'/'low' for glm-5.1/glm-4.5/glm-4.7 on native zai was forwarded
   to Z.AI unchanged and silently ignored — contradicting the PR's stated
   UI/coercion agreement invariant.

   Root cause: the ZAI gate returns [] to mean 'known-empty' (no
   reasoning_effort at all), but the coercion path treated all [] as
   'ambiguous/unknown, preserve verbatim'. Fixed by extracting the ZAI decision
   into _zai_glm_reasoning_efforts_supported (True/False/None sentinel) shared by
   both the filter and coercion, then special-casing the known-False result in
   coerce_reasoning_effort_for_model to return '' (send no field). The nesquena#3505
   preserve-verbatim behavior for genuinely-unknown models on non-zai providers
   is unchanged.

Added 10 regression tests (all fail before the coercion fix, pass after):
- All 6 levels (max..minimal) coerce to '' for each pre-5.2 GLM + glm-4.7
- All 4 aliases (glm/z-ai/z.ai/zhipu) resolve through the same coercion gate
- GLM-5.2 preserves all 6 levels verbatim
- Regression guard: unknown model on custom: provider STILL preserves verbatim

State layer: agent.reasoning_effort config + the value forwarded to Z.AI.
Invariant now fully holds: UI offers no options for pre-5.2 GLM AND coercion
sends no reasoning_effort field for any stored level on those models.

* fix(reasoning): preserve ZAI GLM 4.5-5.1 thinking toggle when effort ladder empty

Address nesquena-hermes round-2 review on nesquena#6219: returning [] for the effort
ladder on sub-5.2 GLM models hid the entire reasoning chip in the composer
(static/ui.js:4932 treats empty supported_efforts as 'no reasoning control at
all'), silently regressing the working thinking on/off toggle for GLM-4.5/4.6/
5.0/5.1 users. Per Z.AI's own docs, those models accept the thinking
{type:enabled|disabled} toggle even though they do not accept the
reasoning_effort intensity ladder.

Fix decouples thinking-toggle capability from the effort ladder:

Backend (api/config.py):
- Refactor _zai_glm_classification returns one of 'effort' (GLM-5.2+), 'thinking'
  (GLM-4.5 up to but not including 5.2), 'forced' (GLM-4.7 family), or None
  (non-zai / non-GLM). Single source of truth shared by all three consumers.
- _zai_glm_reasoning_efforts_supported now wraps classification for the coercion
  contract (unchanged behavior).
- _zai_glm_thinking_toggle_supported returns True for 'effort' or 'thinking',
  False for 'forced', None otherwise.
- get_reasoning_status gains a supports_thinking_toggle field = bool(supported)
  OR (zai_thinking is True). Non-zai providers default to bool(supported_efforts)
  so their chip-visibility behavior is unchanged.

Frontend (static/ui.js):
- New _currentReasoningToggleSupported state var (default undefined = treat as
  true so legacy responses without the field do not newly hide the chip).
- _applyReasoningChip shows the chip when hasEffortLadder OR toggleSupported.
  Empty efforts + toggle=True keeps the chip visible with just the None/On
  control (the existing _applyReasoningOptions already shows 'none' when the
  ladder is empty). Empty efforts + toggle=False (GLM-4.7 forced) hides it.
- Profile-transition and fetch-failure resets now pass
  supports_thinking_toggle:false alongside the empty efforts so the chip hides
  during the unknown-state window, matching the prior reset contract.

Tests (30 new):
- _zai_glm_classification parametrized across all three tiers + aliases + defer
- get_reasoning_status supports_thinking_toggle per tier (GLM-5.2 both, GLM-4.6
  toggle-only, GLM-4.7 neither, non-zai defaults to effort capability)
- Frontend _applyReasoningChip behavior via node driver: empty efforts +
  toggle=True stays visible, toggle=False hides, effort ladder alone is
  sufficient, absent field keeps prior behavior

State layer: agent.reasoning_effort config + supports_thinking_toggle field in
/api/reasoning + composer chip visibility. Invariant: the chip is hidden ONLY
when the model supports neither the effort ladder nor the thinking toggle
(GLM-4.7 forced, or genuinely non-reasoning models); GLM-4.5-5.1 retain the
working On/None control they had before the round-1 effort gate.

* fix(reasoning): make ZAI thinking toggle two-way + force GLM-4.7 stored none

Address nesquena-hermes round-3 review on nesquena#6219 — two SILENT gaps in the
thinking-toggle path, plus a click-handler sibling I found while auditing.

Gap #1 — ONE-WAY toggle for GLM-4.5/4.6/5.0/5.1 (api/config.py:4247,
static/ui.js:4902, static/index.html:757):
The round-2 fix kept the chip visible for thinking-tier models but the only
rendered dropdown option was 'None' (the HTML had no Default option, and
set_reasoning_effort rejected empty effort with 400). So a GLM-4.6 user could
turn thinking OFF but never back ON — worse than the original bug.

Fix:
- set_reasoning_effort now accepts empty effort as 'clear the override' (removes
  agent.reasoning_effort so the provider default takes effect). Invalid values
  still raise ValueError.
- static/index.html gains a <div data-effort=''>Default</div> option.
- _applyReasoningOptions always shows both Default ('') and None alongside the
  effort ladder, so a thinking-tier model (empty ladder + toggle=true) renders
  an operable Default+None two-state control.
- Click handler (ui.js:5106) checks option presence (if(opt)) not truthiness
  (if(effort)) — the old check silently ignored data-effort='' clicks, which
  would have left the Default button dead even after the HTML/backend changes.

Gap #2 — GLM-4.7 not forced when 'none' stored (api/config.py:3984, :3857):
When GLM-4.7 had agent.reasoning_effort=none configured, coercion preserved
'none' via the early return at line 3985, so streaming built disabled reasoning
for a model that forces thinking on regardless. Separately, when the raw
capability source listed 'none', resolve_model_reasoning_efforts reattached it
to GLM-4.7's supported_efforts (['none']), leaking an 'off' option to the UI
for a forced-thinking model.

Fix:
- coerce_reasoning_effort_for_model checks _zai_glm_classification == 'forced'
  BEFORE the generic 'none' early-return, coercing stored 'none' to '' (default
  = thinking on) for forced models.
- resolve_model_reasoning_efforts returns [] early for forced-tier models,
  skipping the 'none' reattachment entirely.

Tests (17 new):
- Gap #2: coerce('none', glm-4.7) -> '', regression guard that non-forced GLM
  still accepts 'none'; resolve does not reattach 'none' for forced but DOES
  for thinking-tier; end-to-end get_reasoning_status for forced+stored-none.
- Gap #1 backend: set('') clears the key (no raise), set('garbage') still
  raises, all 7 valid levels still save.
- Gap #1 frontend: three new node-driver tests asserting the dropdown exposes
  both Default and None for thinking-tier (two-state), Default+None+ladder for
  effort-tier, and the off->on->off round trip keeps both visible throughout.
- Updated test_reasoning_show_hide.test_set_reasoning_effort_rejects_invalid to
  reflect the new contract (empty accepted, garbage rejected).

Regression gate: gap#2 coerce tests fail without the forced-tier check (3
failures); gap#1 backend test fails with 'ValueError: effort is required'
without the empty-acceptance change. 219 passed, 1 pre-existing skip across
the full reasoning + chip + config-cache surface.

State layer: agent.reasoning_effort config + supports_thinking_toggle field +
composer dropdown options. Invariant: the thinking toggle is now genuinely
two-way for GLM-4.5-5.1 (Default=on, None=off, both always visible) and
GLM-4.7 forced-thinking never offers 'none' in any path (coerce, resolve,
status, or UI).

* CHANGELOG: GLM per-version reasoning controls (nesquena#6219)

---------

Co-authored-by: Ruby Hartono <58564005+rh-id@users.noreply.github.com>
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
@webtecnica

Copy link
Copy Markdown
Owner Author

This PR is superseded — the fix was already merged into master via release commit 0a31a4a1 (PR nesquena#6245nesquena#6253), which adds the more comprehensive mobile-aware _openProfileSwitchSessionBrowser() fallback. After rebasing, the PR branch is identical to master with zero diff. Closing as completed.

@webtecnica

Copy link
Copy Markdown
Owner Author

Closing — already addressed in master via a release commit.

@webtecnica webtecnica closed this Jul 21, 2026
webtecnica added a commit that referenced this pull request Aug 13, 2026
…SE relay (nesquena#6961)

Read/surface half of the maintainer's split for PR nesquena#6961 (child approval
routing, nesquena#6943). The resolve half (#1/#2/#3) stays in a follow-up gated on
the agent contract (agent#82009).

#4 (CORE): scope the child->parent cache by canonical state-db/profile path
and only cache positive lookups, so a miss under one profile can no longer
poison another profile's identical child id, and a late state.db write is
picked up on the next lookup. Adds invalidate_child_parent_cache().

#5 (SILENT): use one aggregate projection (own queue + delegated-child
queues, deduped by stable approval id / gateway mirror token) unconditionally
on all three surface paths — sidebar attention summary, /api/approval/pending,
and the SSE initial snapshot — so a parent-with-1 + child-with-1 now reports
count 2 instead of 1.

#6 (SILENT): publish the aggregate parent head/count to the parent's SSE
subscribers whenever an owned child queue changes (submit_pending,
submit_gateway_pending_mirror, retire_gateway_pending_mirror,
resolve_gateway_pending_local, resolve_child_approval_locked), so a pure-SSE
parent consumer sees child enqueue/resolve without waiting for the 1.5s poll.
webtecnica added a commit that referenced this pull request Aug 25, 2026
…SE relay (nesquena#6961)

Read/surface half of the maintainer's split for PR nesquena#6961 (child approval
routing, nesquena#6943). The resolve half (#1/#2/#3) stays in a follow-up gated on
the agent contract (agent#82009).

#4 (CORE): scope the child->parent cache by canonical state-db/profile path
and only cache positive lookups, so a miss under one profile can no longer
poison another profile's identical child id, and a late state.db write is
picked up on the next lookup. Adds invalidate_child_parent_cache().

#5 (SILENT): use one aggregate projection (own queue + delegated-child
queues, deduped by stable approval id / gateway mirror token) unconditionally
on all three surface paths — sidebar attention summary, /api/approval/pending,
and the SSE initial snapshot — so a parent-with-1 + child-with-1 now reports
count 2 instead of 1.

#6 (SILENT): publish the aggregate parent head/count to the parent's SSE
subscribers whenever an owned child queue changes (submit_pending,
submit_gateway_pending_mirror, retire_gateway_pending_mirror,
resolve_gateway_pending_local, resolve_child_approval_locked), so a pure-SSE
parent consumer sees child enqueue/resolve without waiting for the 1.5s poll.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Not run ‘/sessions’ slash command

1 participant