Skip to content

Action Runtime: honest-status across gateway exec paths (+ model-picker, SessionState) - #43039

Closed
claw3649 wants to merge 16 commits into
NousResearch:mainfrom
claw3649:feat/action-runtime-honest-status
Closed

Action Runtime: honest-status across gateway exec paths (+ model-picker, SessionState)#43039
claw3649 wants to merge 16 commits into
NousResearch:mainfrom
claw3649:feat/action-runtime-honest-status

Conversation

@claw3649

@claw3649 claw3649 commented Jun 9, 2026

Copy link
Copy Markdown

Summary

Makes the Hermes gateway's exec contract honest (a failed action is always detectable, never looks like success), unifies the exec handlers behind a single Action Runtime schema, hardens model-picker + gateway infra reliability, and lands Phase 3 Step 1 (a typed SessionState). All changes are additive on the wire — the three frontends (web / desktop / ui-tui) need no change.

Grounded in a new architecture design doc (docs/architecture/central-brain-openclaw.md) describing the Orchestration Core + Action Runtime split and its phased migration.

What's in here (7 commits)

Commit What
fix(model-picker) refresh keyless local model lists (drop ollama rm'd models, preserve pinned subsets); always-live cache for custom/local with stale-beats-empty fallback; pin local Gemma 4 26B to its served 64k num_ctx; web/desktop picker surfaces rejected switches inline + rolls back the optimistic model
fix(gateway) launchd graceful SIGUSR1 restart with hard-stop fallback + PID-reuse-safe SIGKILL; venv launcher re-checks the interpreter is ≥3.11 before re-exec
feat(action-runtime) new action_runtime/ package: unified ExecutionTask / ExecutionResult contract + shell/cli/plugin/slash adapters, each with a byte-identical to_wire(to_result(x)) == x round-trip
refactor(gateway) honest-status across all exec paths (shell.exec / cli.exec / slash.exec / plugin) routed through the Action Runtime; structured result.error on rejected /model switches and plugin failures (keys off _SlashSideEffect.kind, not message text); regression coverage that didn't exist before
docs(architecture) the Central Brain + Action Runtime design + decision log
refactor(gateway) Phase 3 Step 1 — introduce SessionState (a dict subclass + typed accessors) at the two session creation sites; zero behavioral change, safety-analyzed
docs record Phase 3 Step 1 in the decision log

Verification

  • action_runtime round-trip + contract: 13
  • gateway protocol suite (test_protocol.py, byte-compat oracle for the wire): 65
  • session lifecycle (test_tui_gateway_server.py): 208 ✓ (the 1 remaining failure — browser_manage — is a pre-existing environment case that fails on main too)
  • SessionState unit: 4
  • model-layer (model_switch / models / model_metadata / picker): green; the 2 prior test_user_providers_model_switch regressions are fixed

Note: running test_protocol.py + test_tui_gateway_server.py in one process shows 4 cross-file test-pollution failures — these are pre-existing on main (confirmed via a clean-HEAD worktree), not introduced here, and don't appear in per-file runs.

Scope / not included

  • The rich ExecutionResult fields (status / error.retryable / side_effects / task_id) are produced but not yet consumed — consumers (idempotency, retry, the optimistic-update race fix) are Phase 4.
  • Phase 3 Steps 2–5 (move the history_lock turn-lifecycle + compaction CAS into SessionState methods; then the Brain host) are deliberately left for a focused follow-up — they're concurrency-refactor work where rushing risks hard-to-detect bugs.

🤖 Generated with Claude Code

claw3649 and others added 7 commits June 10, 2026 02:01
…res in the UI

Model-resolution reliability and picker UX, including the corrections found in
review of the in-flight batch:

- model_switch: discover keyless local endpoints (ollama/llama.cpp/LM Studio)
  so `ollama rm`'d models stop lingering in the picker — but only when no
  explicit `models:` subset is pinned, preserving an intentional narrowing
  (parity with the custom-providers section; fixes 2 test regressions).
- models: bypass the disk cache for custom/local providers (always live),
  falling through to the shared stale-beats-empty path so a transiently
  unreachable endpoint keeps its last-known-good list instead of going empty.
- model_metadata: pin local Gemma 4 26B to its served 64k num_ctx ahead of the
  /api/show GGUF-max probe so the token budget can't over-run and silently
  truncate; reconcile a stale cache entry in place instead of re-probing every
  call (the previous guard never converged).
- web/desktop picker: await the /model switch and surface a rejected switch
  (result.error) inline — keep the dialog open, roll back the optimistic model,
  toast benign advisories — instead of closing as though it worked.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- launchd_restart: try a graceful SIGUSR1 self-restart before terminate, with a
  hard-stop fallback; bind the force-kill to the freshly-resolved live PID (not
  the captured one) so PID reuse can't SIGKILL an unrelated process.
- hermes launcher: re-exec into ./venv/bin/python on Python <3.11, but only
  after verifying that interpreter is itself >=3.11 — a stale venv no longer
  silently bypasses the version guard and crashes later with an opaque error.
- tests: cover the SIGUSR1 graceful path and reconcile the terminate-drain
  fallback test with the new graceful-first behavior.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ontract

Phase 2 of the Central Brain + Action Runtime design (docs/architecture): a
single honest-status schema the gateway's exec handlers funnel through.

- contract: ExecutionTask / ExecutionResult / Status / ErrorType / ExecError /
  SideEffect / NeedsInput / Constraints. `outputs` is a lossless payload bag so
  each handler's native shape round-trips back to its exact wire dict.
- adapters: shell / cli / plugin / slash, each a pure (to_result, to_wire) pair
  with a byte-identical round-trip guarantee.
- tests assert the round-trip identity (the byte-compat oracle).

Pure dataclasses with no dependency on the gateway (server imports this, not
the reverse). Nothing consumes the rich fields yet — that is Phase 4.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… Runtime

- slash.exec: report a rejected live side-effect (/model, busy) and a raised
  plugin handler as a structured result.error (additive — TUI/web that read
  only output/warning are unaffected), keying off _SlashSideEffect.kind rather
  than prefix-matching the message text.
- shell.exec / cli.exec / slash.exec / plugin path now build an ExecutionResult
  and render it back through the adapters — byte-identical wire, so the three
  frontends need no change. A non-zero exit is an honest FAILED result whose
  wire shape (result.code) is unchanged.
- desktop typed `/model` path: surface result.error instead of rendering a
  rejected switch as a success line; add error? to SlashExecResponse.
- add honest-status regression coverage for the plugin error field and for the
  already-honest shell.exec / cli.exec paths (none existed before).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A design doc for separating the Orchestration Core (reasoning/planning/memory)
from the Action Runtime (execution/tool-use), grounded in the current Hermes
gateway. Covers the decided terminology (OpenClaw = compat/reference only), the
ExecutionTask/ExecutionResult contract, the phased migration (1a honest-status →
2 unified path → 3 SessionState + Brain-host → 4 intent/idempotency → 5
multi-runtime), cross-cutting rules (additive-first, test-first, observability,
idempotency), and a decision log tracking what has landed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…n sites

Phase 3 Step 1 (Central Brain + Action Runtime design, §11). The per-session
state was an untyped dict at _sessions[sid] (~58 refs, ~30 keys, locks floating
as dict entries). Introduce a SessionState type to anchor the decomposition.

SessionState subclasses `dict`, so every existing session["key"] / .get /
.setdefault / "k" in session call keeps EXACT dict semantics — zero handler
churn. A safety analysis confirmed the gateway has no membership test, bare pop,
or iteration that relies on the session value dict, and that absent keys must
stay absent (a dict subclass gives that for free, so no field defaults need
tuning — notably agent_ready stays absent on the eager path and personality
keeps its config fallback).

Typed @Property accessors (history, history_lock, running, agent, …) let
handlers migrate to attribute access incrementally in later steps; the locks
stay dict items until the subscript sites are converted. Wraps the two real
creation literals (_init_session, session.create); test-constructed plain dicts
still work since SessionState IS a dict.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
SessionState type introduced and committed; note the next step (Step 2 lock-core
methods). Also reflects the verified AIAgent instantiation-site count.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@alt-glitch alt-glitch added type/refactor Code restructuring, no behavior change comp/gateway Gateway runner, session dispatch, delivery comp/cli CLI entry point, hermes_cli/, setup wizard comp/tui Terminal UI (ui-tui/ + tui_gateway/) P2 Medium — degraded but workaround exists labels Jun 9, 2026
claw3649 and others added 3 commits June 10, 2026 14:08
…robe

Generalizes the root cause behind the Gemma4-26B 64k pin: Ollama's /api/show
reports the GGUF training max, which over-states the window a LOCAL server
actually serves (the user-set Modelfile num_ctx) and silently truncates
conversations once they outgrow it. Steps 2b and 5e now resolve local
endpoints num_ctx-first (query_ollama_num_ctx) while hosted servers keep the
GGUF-first probe (their users can't set num_ctx); discovered values persist
under the same lmstudio cache exclusion the sibling steps use. The explicit
Gemma pin stays as a tested guard above these steps.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Opening the picker live-probes every custom/local endpoint (keyless-local
discovery in sections 3/4 and the live "custom" branch of provider_model_ids);
with a server down each open burns the full HTTP timeout. Add a 30s in-process
memo around fetch_api_models — keyed by endpoint + credential fingerprint and
pinned to the exact function object so monkeypatched tests stay hermetic
(plus an autouse clear in tests/hermes_cli). Empty results and exceptions are
never memoized, so a down server still retries on the next open and the
stale-beats-empty disk-cache semantics are untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…istry

Pre-existing cross-file pollution: the dispatch-pool tests overwrite entries in
the module-global _methods registry (slash.exec, session.compress, …) without
restoring them, so four session.compress/snapshot tests in
test_tui_gateway_server.py fail whenever both files run in one process. The
server fixture now snapshots and restores _methods in place (same dict object)
plus the directly-assigned module globals (_real_stdout, _hermes_home,
_cfg_cache/_cfg_mtime/_cfg_path). Tests-only; both files also stay green alone.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@claw3649
claw3649 force-pushed the feat/action-runtime-honest-status branch from 8a4e0cb to c962b39 Compare June 10, 2026 07:08
@claw3649

Copy link
Copy Markdown
Author

Addressed the self-review follow-ups in 3 new commits:

  • fix(model_metadata) — generalized the root cause behind the Gemma 64k pin: local Ollama endpoints now resolve context num_ctx-first (the served window) instead of the GGUF training max, at both probe sites (steps 2b/5e). Hosted servers keep GGUF-first. The pin remains as a tested guard.
  • perf(model-picker) — 30s in-process memo around fetch_api_models so opening the picker doesn't re-probe every custom/local endpoint each time (empty results/exceptions never memoized; monkeypatch-hermetic via function-identity pinning + autouse clear).
  • test(tui_gateway) — fixed the pre-existing cross-file pollution disclosed in the PR description: dispatch-pool tests were overwriting the shared _methods registry without restoring it. Combined test_protocol.py + test_tui_gateway_server.py runs are now clean (only the environment-dependent browser test remains).

Verification: model-metadata suites 132 ✓ · picker suites baseline-exact (the 4 known live-ollama env failures only) ✓ · combined gateway run 273 passed ✓ · action_runtime + protocol 127 ✓

claw3649 and others added 4 commits June 10, 2026 14:31
Phase 3 Step 2 (Central Brain + Action Runtime design, §11). The compaction
snapshot + compare-and-swap, the turn-release finally, and the one-shot
agent-build guard now live as SessionState methods — snapshot_history,
commit_compaction, begin_turn, end_turn, build_once — each a verbatim lift of
the server.py block it replaced (same lock, same .get defaults, same order).
Five gateway sites migrated; sites whose critical sections interleave extra
work with the CAS (the prompt.submit busy-gate + truncate, the poller gates,
bare running-flag cleanups, the mid-turn history CAS that logs under the lock)
stay deliberately inline to preserve atomicity, each noted in place.

Test helpers that feed the migrated paths now construct SessionState (a dict
subclass, so semantics are unchanged); 9 unit tests pin the method contracts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…esult wire

Phase 4 (Central Brain + Action Runtime design, §11): make the rich
ExecutionResult consumable.

- slash.exec accepts an optional task_id and echoes it additively (the wire is
  byte-identical when absent — existing protocol tests pass unmodified).
- result_to_wire_rich renders the full contract: task_id, status, outputs,
  error {type, retryable, message}, side_effects.
- New task.submit RPC: pilot scope intent="slash" only (reversible commands,
  e.g. /model), other intents → 4030. The slash.exec body is extracted into a
  shared _slash_exec_core returning the ExecutionResult; slash.exec renders the
  legacy wire exactly as today, task.submit renders the rich wire.
- Idempotency: a locked in-process store (10-min TTL, 1024-entry cap) replays
  the recorded result with replayed=true instead of re-executing; task_id
  defaults to a fresh uuid4.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…-flight token

Phase 4 race fix (from the PR review): selectModel applied optimistic switches
with no in-flight correlation, so a slow switch A failing after a faster
switch B succeeded rolled the store back to the pre-A model, clobbering B.

Each selectModel call now captures a monotonically-increasing token; once a
newer call starts, the older call is stale — a stale success commits nothing
and a stale failure skips the store/cache rollback (the snapshot predates the
newer switch) while keeping the failure toast so the user still learns their
requested switch failed. refreshCurrentModel gains an optional staleness guard
used by the persistGlobal paths; external callers are unaffected. The slash.exec
request also carries an additive task_id for forward correlation; staleness is
decided purely by the local token. Two race tests cover both interleavings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…sion log

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@claw3649

Copy link
Copy Markdown
Author

Landed Phase 3 Step 2 and the Phase 4 pilot (4 new commits):

  • refactor(gateway) — the compaction snapshot+CAS, turn-release finally, and one-shot agent-build guard now live as SessionState methods (snapshot_history / commit_compaction / begin_turn / end_turn / build_once), each a verbatim lift of the inline block it replaced. Sites whose critical sections interleave extra work (the prompt.submit busy-gate+truncate, poller gates, the mid-turn CAS that logs under the lock) stay deliberately inline with notes — atomicity preserved over force-fitting.
  • feat(action-runtime) — new task.submit RPC (pilot: intent="slash" only, per the design doc's reversible-first rule): the slash.exec body is extracted into a shared _slash_exec_core, so slash.exec keeps its byte-identical legacy wire while task.submit returns the full rich result (status / error.retryable / side_effects / task_id). Idempotency: re-submitting the same idempotency_key replays the recorded result (replayed: true) without re-executing. slash.exec also echoes an optional client task_id additively.
  • fix(desktop) — the optimistic model-switch race from the review: an in-flight token makes a slow switch that resolves after a newer one stale — it no longer commits or rolls back the store (the failure toast still fires), so a late failure can't clobber a newer successful switch.

Verification: gateway suites 165 + 208 + combined 278 ✓ (only the known env-dependent browser test) · desktop race tests 4/4 + clean typecheck ✓ · all pre-existing protocol assertions unmodified (byte-compat oracle).

…us surfacing

Upstream moved the web model picker's chat path from slash.exec to config.set
and added the expensive-model confirmation flow (pendingConfirm). Resolution
keeps upstream's architecture and folds our honest-status work into it: all
three modes (standalone onApply, gateway config.set, legacy onSubmit fallback)
funnel through one awaited try/catch — a failed switch surfaces inline via
submitError and the dialog stays open; a benign post-switch warning fires the
new onWarning prop, which ChatSidebar toasts. The now-dead slash.exec
onModelSubmit path is removed with upstream.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…sh refactors, keep honest-status surfacing

Upstream restructured executeSlashCommand into a slash-action table with
a generic runExec path (and then made slash commands registry-driven in
#42351); /model-with-args now rides runExec, so our result.error
honest-status check lives there — covering every exec-style command
instead of just the typed /model path. The refreshCurrentModel guard
keeps BOTH protections: upstream's live-session-owns-the-footer guard
and our in-flight staleness token. The model-controls test file unions
upstream's two refreshCurrentModel tests with our four selectModel
rollback/race tests (6/6 green under --environment jsdom).

Desktop src/ suite shows the byte-identical failure set to pure
origin/main (6 pre-existing); python gateway suites green.

Note: this replaces an earlier broken push of the same resolution — a
mid-merge stash for a baseline measurement silently dropped MERGE_HEAD,
so 46a668be2 landed as a single-parent commit with partial upstream
content. Reset and redone as a true merge; baselines are measured in
detached worktrees from now on.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@claw3649
claw3649 force-pushed the feat/action-runtime-honest-status branch from 46a668b to dae2efb Compare June 11, 2026 03:37
@claw3649

Copy link
Copy Markdown
Author

Friendly ping for a review on this one 🙏

Quick triage signals to make this cheap to pick up:

  • Scope: honest-status across the gateway exec paths — purely additive (result.error field; every existing wire byte-identical, pinned by exact-dict protocol tests), plus the model-picker fixes and the SessionState/task.submit groundwork described in the description.
  • Freshly rebased: merged main twice this week (latest: adopted the slash-action-table + registry-driven slash refactors; our error check now lives in the new runExec, covering every exec-style command). Currently MERGEABLE, suites green.
  • Stack note: Phase 3: SessionState attribute access + flag-gated BrainHost seam #43422 (draft) and three follow-on branches are staged behind this — this PR is the bottleneck root, so a review here unblocks the whole chain.
  • Same-author track record this week if useful for calibration: chore(ui-tui): clear all 34 pre-existing eslint errors — zero behavior change #43895 (lint cleanup) already approved by @tonydwb.

Happy to split this further if the current size is the blocker — say the word and I'll carve it.

@claw3649 claw3649 closed this Jun 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/cli CLI entry point, hermes_cli/, setup wizard comp/gateway Gateway runner, session dispatch, delivery comp/tui Terminal UI (ui-tui/ + tui_gateway/) P2 Medium — degraded but workaround exists type/refactor Code restructuring, no behavior change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants