Drop the 3-line facade wrapper (hermes_cli/gateway.py is already 3x the facade
threshold) and call the existing _ensure_user_systemd_env() directly under
`is_linux() and INVOCATION_ID` — the same Linux gate the process_registry seam
uses, instead of os.name == "posix". The fail-closed test now targets
_ensure_user_systemd_env() itself. Hedge the scope-unavailable error text: the
probe also returns False when systemd-run is missing or times out, so the
D-Bus diagnosis is the usual cause, not the only one.
A system-level unit (/etc/systemd/system, User=<someone>) is exec'd with neither
XDG_RUNTIME_DIR nor DBUS_SESSION_BUS_ADDRESS, and a process environment is fixed
at exec time. 'systemd-run --user --scope' therefore fails for the whole lifetime
of that gateway even after the user manager is up and /run/user/<uid>/bus is
reachable. That is the seam every restart-safe worker crosses
(restart_safe_gateway_child_argv), and it fails closed by design — so on headless
systemd installs every agent-driven cron job and every Kanban dispatch died at
launch, ~26ms in, with nothing but 'error' on the job row.
_ensure_user_systemd_env() already derives both values from our own uid and adopts
them only when the runtime dir is really ours and the socket really exists; it was
just wired exclusively to the systemctl management paths, never to the gateway's
own boot. Call it from run_gateway() — the single in-process boot every entry point
goes through — so the adoption precedes every worker-environment snapshot (cron
builds its env after the scope check, Kanban before it, so fixing this at the
dispatch seam would only fix one of them).
The fail-closed posture is unchanged: with no user manager at all the probe still
reports unavailable and dispatch still refuses. That refusal now names the remedy
in the message the operator actually reads (it is stored as the cron execution's
error), instead of only the symptom.
Fixes#104893
The new standalone test file duplicated TestGitBashExternalProgramProbe's
harness; one assertion on the recorded kwargs covers the #78820 contract.
The sibling ASLR-probe assertion is dropped (unchanged code, already green
on main). Comment trimmed to the WHY.
`_bash_starts()` in tools/environments/local_gitbash_probe.py ran bash.exe
with capture_output=True but no stdin=, so the MSYS2 child inherited the
TUI gateway's stdin pipe. The MSYS2 runtime switches that shared pipe to
PIPE_NOWAIT; the gateway's next sys.stdin.readline() then fails with
ERROR_NO_DATA, which the CRT maps to OSError(EINVAL), and the gateway
exits with code 1 ("gateway exited") on the first terminal call.
The sibling probe _mandatory_aslr_enabled() already passes
stdin=subprocess.DEVNULL; this brings _bash_starts() in line with it.
Add tests/tools/test_gitbash_probe_stdin.py asserting both probes forward
stdin=DEVNULL to subprocess.run (fails on the pre-fix source).
_resolve_child_fallback_chain re-implemented hermes_cli.fallback_config's entry
validation to log per-index warnings, wrapped a pure function in try/except, and
carried two names (routing_cfg / fallback_cfg) for one argument. It now delegates
to get_fallback_chain() and keeps the single warning for "no usable routes";
the facade re-export is dropped (import from the defining module). Tests collapse
to the parametrized decision table plus the pin-derivation, real-config-loader,
review-ownership and activation invariants (22 -> 20 cases, 417 -> ~230 lines).
Stress run with a real orchestrator subagent showed the per-task keys buried in results[] were not relayed
upward; the same prose lines now sit at the top of the sync delegate_task result. evals/subagent_process_handoff/
stress_handoff_live.py runs 12 real parent+child scenarios (handoff, orphan, unread, clean read, cap, exited refusal,
mixed fan-out, parent controlling the inherited process, sibling theft incl. adversarial, nested orchestrator,
5-way burst) and scores them against runtime accounting, never model prose.
A process that finishes while the child is alive needs no handoff, but if the child never
polls/waits/logs it, the result vanished: the completion notice is suppressed in the parent
and the child's summary never mentions it. Finalization now attaches exit code + output
tail as unread_completions, rendered in the parent's delegation notice.
A child's background processes are killed at its teardown and their
notify_on_complete notices are suppressed in the parent, yet the child's
terminal result still said `notify_on_complete: true` and the parent's
delegation notice said nothing about processes left behind. Orchestrators
believed "CI watcher running" and waited on a completion that could never
arrive (recurring in the Sep 7 campaign sessions).
- process_manage(action="handoff", session_id, data="<purpose>"), children
only: process_registry.transfer_ownership flips owner_task_id/task_id/
session_key to the parent under the registry lock, so the completion is
stamped with the parent's owner at exit, passes the parent's sa- filter,
and is reaped by the parent, not the child. Cap 3 per child; an exited,
foreign, or non-child request is a tool error. The purpose rides the
event as handoff_note and renders in the parent's notice.
- Child terminal(background=True, notify=True) now returns
notify_on_complete=false plus a note: wait, kill, or hand off.
- _ChildRun.account_background_processes records handed_off_processes and
orphaned_processes on the result before cleanup kills the leftovers; the
parent's delegation block renders both.
Self-review follow-ups on the F821 sweep:
- plugins/platforms/sms/adapter.py: the optional-import block now sets AIOHTTP_AVAILABLE like
the homeassistant / webhook / whatsapp_cloud adapters, and both call sites test the flag.
Removes the `if not aiohttp is not None:` double negation left by inlining
`_aiohttp_available()`.
- tools/mcp_tool_sampling.py: `call_context` defaults to `lambda: None` so the use site is a
single call instead of an Optional guard; the only None caller was a test. The
`from __future__ import annotations` was noise (`Context` is a runtime import). Comment
names the actual cycle (mcp_tool_server_run imports this module).
- gateway/platforms/helpers.py: drop the `from __future__ import annotations` — the only
MessageEvent annotations are attribute-target locals, which are never evaluated.
- tests/gateway/test_telegram_audio_vs_voice.py, test_video_context_note.py: module-level
`from gateway.run import GatewayRunner` like the ~100 sibling files; the
TYPE_CHECKING block + `# type: ignore[name-defined]` were contradicting each other.
(tests/e2e/conftest.py and test_feishu.py keep TYPE_CHECKING deliberately: they stub
telegram/discord before importing, and FeishuAdapter is gated on optional lark_oapi.)
Mutation check: neutralising the thunk read (`captured = None`) fails
test_captured_context_is_replayed_in_consent_call; restored → 14/14 green. ty on the three
touched production files vs origin/main: 0 new, 6 resolved.
Breaks the two import cycles that forced Protocol stand-ins in the F821 sweep, so the two
sites now name the real types.
gateway/platforms/event.py (new leaf): MessageType, ProcessingOutcome, MessageEvent moved
out of base.py verbatim. Their only dependency is gateway.session.SessionSource; base.py
imported helpers.py at module level, so helpers could not name MessageEvent. Now
TextBatchAggregator is typed by the real MessageEvent. 249 importers repointed
(`from gateway.platforms.base import` -> `.event`, preserving each import's layout);
gateway.platforms.__init__ re-exports from .event. The three revert-scheduled PLUGIN-COMPAT
pointers that named these symbols (gateway.slash_commands → MessageType, dingtalk → MessageType,
photon → ProcessingOutcome) and their COMPAT_MANIFEST rows now target gateway.platforms.event.
Docs updated: ADDING_A_PLATFORM.md, adding-platform-adapters.md (en + zh-Hans).
tools/mcp_tool_sampling.py: ElicitationHandler no longer holds a back-reference to its
MCPServerTask (mcp_tool imports sampling, so the task type cannot be named there). It only
ever read owner._pending_call_context, so it takes `call_context: Callable[[], Context | None]`
and MCPServerTask passes `lambda: self._pending_call_context`. The consent call is one
`functools.partial`, run directly or inside the captured Context.
ty on the 11 touched production files vs origin/main: 0 new diagnostics, 14 resolved.
(The one `source: SessionSource = None` diagnostic moves with the class; typing it Optional
exposes ~60 unguarded call sites — separate follow-up.)
Tests: tests/gateway + tests/plugins + tests/tools + touched files, 18,235 passed; the 31
failures reproduce identically on origin/main (macOS /private/tmp, systemd socket,
long-path fixtures, live-service tests).
Sweep of `ruff check . --select F821 --target-version py311`: 2,234 hits. 2,201 are left
alone on purpose: tui_gateway (2,169; bind_module rebinds bodies onto server.py globals,
all names verified to resolve there), the Feishu adapter (27; globals().update() SDK
binding) and the godmode script (5; dead standalone script). The other 33 were all
genuine defects. No lint config change; no TYPE_CHECKING escape hatches — every
annotation names a real, imported type; ty on the touched files: 0 new diagnostics.
- gateway/slash_commands.py: HISTORY_UNREADABLE never imported after #102117
→ NameError on the /btw error branch (same one-liner as #102952).
- gateway/platforms/whatsapp_common.py: `-> Path` return annotation with no Path
import (the body uses `_Path`). Never raised at runtime thanks to
`from __future__ import annotations`, but `typing.get_type_hints()` and ty
both fail on it.
- gateway/run.py: ActivityProvenance imported at module level
(agent.session_activity has no gateway deps); stringly annotation and the
lazy in-function import are gone.
- tools/patch_parser.py: PatchResult imported at module level; real return
annotation. The "avoid circular import" lazy import guarded a cycle that
does not exist (file_operations_common never imports patch_parser).
- gateway/platforms/helpers.py: base.py imports helpers at module level, so
MessageEvent cannot be named here; TextBatchAggregator only reads .text and
.source, so it is typed by a BatchableEvent Protocol that MessageEvent
satisfies structurally.
- tools/mcp_tool_sampling.py: mcp_tool imports this module, so MCPServerTask
cannot be named here; ElicitationHandler only reads
owner._pending_call_context, typed by an ElicitationOwner Protocol.
- plugins/platforms/sms/adapter.py: aiohttp is an optional dep ([messaging] extra) →
module-level try/except ImportError binding `aiohttp = web = None`, the pattern the
homeassistant / webhook / whatsapp_cloud adapters already use. Retires three lazy
in-function imports and the `_aiohttp_available()` wrapper; `_handle_webhook` typed
`web.Request -> web.Response`.
- plugins/platforms/teams/summary_writer.py: plain module-level `import httpx` — httpx is a
hard core dependency (pyproject `httpx[socks]==0.28.1`), so the lazy import and the
"imported on every CLI start" docstring premise were both wrong (plugin discovery never
imports this module; it is reached only via the Teams adapter / meeting pipeline).
Tests:
- tests/hermes_cli/test_config.py: a test body orphaned by the wave-1 prune
(6b81590c55) sat inside the class as dead code with self/tmp_path unbound
— header restored, so the v11→12 custom_providers migration is covered.
- tests/tools/test_mcp_tool.py: @staticmethod recursing on `self` in the
win32 branch; call portalocker directly.
- tests/test_background_review_list_shapes.py: main() still ran 3 pruned tests.
- tests/agent/test_cursor_optimizations_parity.py: bench() used names only
imported inside a sibling test.
- GatewayRunner / FeishuAdapter / Dict / Optional: missing imports.
Persist paused state, timestamp, reason and no first trigger in the original
locked creation write. Forward the same boolean contract across CLI, tool,
gateway API and dashboard API, validating at the store boundary. Preserve
explicit operator force-run behavior and normal enabled creation.
The live CLI probe also caught the command shim dropping failure return codes;
forward them so invalid creation reports exit 1 rather than success.
Credit earlier atomic-creation work in #78935 and #94952 and the focused
implementation in #104578. The broader manifest staging layer is not imported.
Co-authored-by: Konstantin Khlopkov <konstantin.khlopkov93@gmail.com>
Co-authored-by: Chloé DuPont <321112755+misschloedupont@users.noreply.github.com>
Based on the earliest batching proposal by BrunoBza (#104686) and JoaoMarcos44 structured correction (#104703). Slim redo into topical siblings and shared TUI poller/post-turn routing. Reported by Xipong (#104671). Local shell-to-loopback probes demonstrate 12 to 1 turn dispatches; suites await the campaign test lock.
Co-authored-by: joaomarcos <joaomarcosdias444@gmail.com>
Capture the durable parent session before output readers start, including CLI
and non-notifying spawns. Require that parent or its compression continuation
for retained reads; exact and prefix handles alone do not authorize access.
Live Linux terminal/one-shot linger/fresh-reader A/B: base loses results;
updated owner recovers both streams and exit 7. Unbound, foreign session,
delegated child, and other profile cannot recover the receipt. No notifications
are replayed. Full tools suite is queued behind the campaign test lock.
Follow-up to contributor salvage #104805 for #104511.
Guard device metadata subtype selection with a dictionary check so valid
non-object JSON reaches the existing validation-and-ignore path. Preserve
null handling, cache contents, and normal/device metadata cold-load types.
Extend the existing corrupt-cache invariant rather than adding test functions.
Live filesystem A/B reproduces list/string/number/boolean AttributeError on
the prior head and clean ignore after this change. Nine CLI OAuth wire
scenarios, browser S256 and profile-scoped storage controls pass locally.
Add explicit RFC 8628 device login and oauth.flow selection while keeping
browser PKCE and the SDK runtime refresh path. Reuse issuer/resource
validation, configured client authentication and profile-scoped storage.
Only persist an approved, validated grant; never echo endpoint error bodies.
Slim redo of #104752 by @wjorgensen, replacing duplicate HTTP/storage
wrappers with the existing SDK and two real-wire invariant tests.
Refs #104742
Co-authored-by: Wes Hermes <weshermes@Wess-Mac-mini.localdomain>
Persist a redacted chained traceback in the private run output and expose
redacted last_error in tool and slash listings, including historical errors.
Keep the run_job concise error return unchanged for delivery classification.
Slim redo of liuhao1024's earliest #104545; adds forced redaction and keeps
formatting in a topical sibling. Local SDK/socket A/B verifies diagnosis
visibility plus healthy-script, clearing, and private-file controls.
Canonical tests queued under the campaign lock at commit time.
Co-authored-by: liuhao1024 <sunsky.lau@gmail.com>
Emit the one-shot reason marker outside the CLI facade; parse whole codes before falling back to legacy prose. Explicit coordination and unknown codes cannot be labeled target_busy.
Fixes#104784
Co-authored-by: William Echo <2054936695@qq.com>
Normalize boolean required only at schema positions; lift true property flags into parent arrays. Preserve literal default/const/extension data.
Fixes#104796
Inspired by #104831 and the lifting proposal by @AdJIa.
Recover legacy stringified lists with a warning. Reject malformed shapes and nonstring members without admitting approvals or rewriting user config on read.
Fixes#104779
Co-authored-by: liuhao1024 <sunsky.lau@gmail.com>
Salvage the bounded target-slot design from #104617, using mandatory
word separators to avoid ambiguous repeated matches. Preserve long
payload detection and execution-verb boundaries. Replace the three
candidate tests with two context-loader invariants and document the
heuristic's limits.
Fixes#104609
Co-authored-by: Konstantin Khlopkov <konstantin.khlopkov93@gmail.com>
Carry the existing write fence across Hermes-owned spawn boundaries without
dropping board routing or changing credential policy. Grant dispatcher and
managed tool runtimes explicit task scope; align CLI task mutations with tools.
Verify real shell/CLI descendants, dispatcher startup, and supervised stdio
transport against isolated SQLite boards. This is cooperative runtime scoping,
not OS confinement.
Refs #103974, #104058, #104904
ClawHub's detail endpoint now answers a slug claimed by multiple owners
with 409 AMBIGUOUS_SKILL_SLUG; the bare GET in _skill_detail returned
None for every such slug, so 'skills install clawhub/@owner/slug' (and
the owner/skills/slug URL form) failed at fetch time even though the
requester already knew the owner (#104117).
- _skill_detail forwards expected_owner as the ?owner= query param on
the detail GET (params already flows through _get_json's **kwargs).
- _parse_identifier also accepts the clawhub/@owner/slug combination:
the @ surfaces only after the clawhub/ prefix is stripped, so the
had_at check now re-runs on the stripped form. GitHub-style
owner/repo/skill paths stay rejected.
_paginate_full_list wrapped the paginated list call in try/except TypeError
to detect the mcp 1.x calling convention. The same except also caught
TypeErrors raised INSIDE the modern list call — e.g. a server response
decode failure — and retried with the legacy cursor= keyword, replacing the
real error with a misleading 'unexpected keyword argument cursor' and
making genuine MCP pagination failures undiagnosable.
Probe list_method's signature instead (_list_method_accepts_params): the
legacy cursor= fallback fires only when the method genuinely doesn't accept
the mcp 2.0 params= keyword (or takes **kwargs), so a TypeError from inside
the list call propagates to the caller. Regression tests: the decode
TypeError surfaces and the legacy retry doesn't run; a genuinely 1.x-shaped
method keeps using the cursor fallback.
Three orchestrator failures traced through the Sep 7 gpt-6-astra campaign sessions:
1. delegation.independent_completions (new, default false). #104299 made every
ungrouped task its own completion message, so a 15-task call woke the
orchestrator up to 15 times; one chain received 132 notices and answered
130 of them with "already incorporated". A multi-task call now returns as
ONE consolidated message unless the flag is on; `group` is inert until then.
2. Queued units were killed before they started. Units of one call share a
pool slot but the executor was still sized by slots, so with 15 units live
a new unit queued behind a full pool; the stale monitor's clock ran from
dispatch, interrupted it at 450 s, and the child exited `interrupted 0.02s`
when its thread finally came up (13 such lanes in one session). The
executor now grows to the number of live units and the stall clock arms
when the runner actually starts.
3. The tool text said "do not wait or poll — just continue" without saying
that completions are delivered only BETWEEN turns. A model that never ends
its turn (one 203-minute turn, 717 API calls) never received 40 finished
results. Tool description, dispatch note and completion header now say to
finish independent work, give a one-line status, and end the turn.
The previous commit bumped pyproject.toml and uv.lock but missed the
LAZY_DEPS exact pin for platform.discord, so
test_pyproject_pins_match_lazy_deps_pins and
test_every_lazy_deps_exact_pin_matches_uv_lock fail with
{'brotlicffi': {'platform.discord': {'lazy_pin': '1.2.0.1', 'uv_lock': ['1.2.0.2']}}}.
Update the third registration site to keep all three in lockstep.
Slim adaptation of #83772 to the current schema and failure-hint table.
Generated helpers are module exports on every execution path, not globals.
Correct schema, recovery hints and CLI tip rather than injecting names or
changing the execution boundary. Two registry-driven invariants reproduce
both misleading instructions on main and execute the corrected guidance.
Additional tool fix discovered during campaign #104904.
Original diagnosis and correction: @yuzilongleif-collab (#83772).
Co-authored-by: yuzilongleif-collab <235949691+yuzilongleif-collab@users.noreply.github.com>
check_for_skill_updates() fetched every lock-file entry remotely, even
when the entry's install directory no longer existed, and each fetch had
no wall-clock bound — a few dead sources turned a routine
`hermes skills update` into a multi-minute stall (#104291).
- Entries whose recorded install_path resolves but does not exist are
reported as "orphaned" and skipped without a remote fetch;
unresolvable paths keep the previous fetch behavior.
- Each fetch now runs under a daemon helper thread with a hard timeout
(default 30 s) and degrades to "unavailable" when abandoned.
- `hermes skills check` prints a removal hint for orphaned entries.
Fixes#104291