Skip to content

fix(daemon_pool): support Python 3.14 ThreadPoolExecutor WorkerContext API - #70873

Open
jleechan2015 wants to merge 35 commits into
NousResearch:mainfrom
jleechanorg:fix/daemon-pool-py314
Open

fix(daemon_pool): support Python 3.14 ThreadPoolExecutor WorkerContext API#70873
jleechan2015 wants to merge 35 commits into
NousResearch:mainfrom
jleechanorg:fix/daemon-pool-py314

Conversation

@jleechan2015

Copy link
Copy Markdown

CPython 3.14 refactored ThreadPoolExecutor to pass WorkerContext to _worker. This PR makes _adjust_thread_count version-adaptive.

jleechan2015 and others added 30 commits June 28, 2026 13:13
…tream

Mirrors the adjustment registry pattern (backend_adjustment_registry.py in
mvp_site) applied to fork management. Tracks: what, why, upstream PR status,
and how to verify each deviation is safe to remove on rebase.

Active entries: memory_tool truncation bug (upstream PR pending), slack loop
prevention (upstream PR ready), macOS status fix (upstream PR NousResearch#16).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
(cherry picked from commit 212cd72)
(cherry picked from commit c7f075accbea7876a7f167bcd7b8a073c88c5022)
… comment

(cherry picked from commit 678df8ee7192af82954958c65316347932d052f8)
…ask-completed

(cherry picked from commit abe4442a744c7dc9ddd760314fd359f13fe22fee)
…ded for version bumps) (NousResearch#15)

* [agento] fix: green-gate Gate 3 — auto-PASS dependabot PRs (no CR needed for version bumps)

dependabot[bot] PRs are automated dependency bumps. Requiring a
CodeRabbit review blocks them indefinitely when CR hits rate limits.
Add PR_GATE3_AUTHOR check: if author == dependabot[bot], Gate 3
auto-PASSes. Human-authored PRs continue to require CR APPROVED.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* [agento] fix: set LATEST_CR=APPROVED for dependabot Gate 3 auto-PASS (Gate 5 fast-path)

BugBot caught that LATEST_CR was unset when dependabot PRs auto-PASS Gate 3,
making Gate 5's CR-approved non-blocking fast-path unreachable for those PRs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* [agento] fix: skeptic-cron Gate 3 — dependabot PRs auto-PASS (no CR needed)

Mirrors green-gate Gate 3 dependabot exemption so dependabot version bumps
can be auto-merged by skeptic-cron without requiring a formal CR review.
Adds author field to PR JSON payload so dependabot detection works in the loop.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…arer tokens

setupMcpMailInWorkspace() in agent-base writes MCP_AGENT_MAIL_TOKEN as a
literal Bearer token into .claude/settings.json because Claude Code does not
expand env vars in MCP headers. Previously only machine-local info/exclude
prevented accidental commits. Add repo-level .gitignore entries so all
contributors are protected.

Fixes bead orch-havc (P0).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Gate 3 (CR APPROVED) blocks all PRs because CodeRabbit only posts
COMMENTED reviews without this config. Enabling auto_review allows
CodeRabbit to post APPROVED reviews, unblocking the 7-green merge
criteria.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…t.yaml

Gate 3 (CR APPROVED) was failing on all PRs because CodeRabbit was posting
COMMENTED reviews instead of APPROVED. These two settings are required for
CR to post formal APPROVED review states.
Maps hot-spot files to domains for concurrent-agent conflict detection.
Run install.sh from merge_train repo to wire session/pre-tool hooks.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Configures all declarative lock domains in `file_domains.yaml` with `advisory: true` to support advisory cross-repo lock logging.
…ousResearch#27)

Slack channel-thread replies delivered via the non-streaming queued
follow-up / stream-consumer path could land at the channel ROOT
(thread_ts=None) instead of in-thread. When a run is interrupted by an
injected background-process completion event and drains a queued
follow-up (often across a mid-run context-compression session split),
the reply is sent with _status_thread_metadata.

_progress_thread_id uses a Slack reply-anchor fallback
(source.thread_id or event_message_id), and _progress_metadata honours
it. But _status_thread_metadata derived the value via
_thread_metadata_for_source(source, ...), which keys off
source.thread_id only and returns None when source.thread_id is None —
dropping the anchor even though _progress_thread_id is truthy. The
queued-delivery send passes no reply_to, so _resolve_thread_ts(None,
None) yields None and chat_postMessage omits thread_ts → channel-root
leak.

Mirror _progress_metadata: when _progress_thread_id is truthy and
differs from source.thread_id (the Slack event_message_id fallback),
carry {"thread_id": _progress_thread_id} explicitly. This parallels the
existing Telegram DM fix (b323957) which Slack lacked.

Adds tests/gateway/test_slack_thread_survives_compression.py reproducing
the prod leak (RED before fix, GREEN after).

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…directory (NousResearch#31)

When a Slack bot is installed in multiple workspaces, a human-friendly
channel name that exists in more than one workspace (e.g. two orgs both
with #engineering) silently resolves to whichever workspace was
enumerated first. The actual chat.postMessage then posts via the
primary bot token, which authorizes a different workspace — a
cross-channel Slack misroute where the message lands in the wrong
workspace entirely.

Root cause: gateway/channel_directory.py._build_slack did not record
team_id per workspace entry, so resolve_channel_name() had no way to
tell two workspaces apart and silently picked the first match.

Fix:
1. gateway/channel_directory.py: _build_slack now tracks team_id and
   team_name on every entry. resolve_channel_name refuses to resolve
   a name when matches span multiple workspaces. New
   resolve_channel_name_strict() returns (chat_id, team_id) so callers
   can route to the correct workspace's bot token. New
   lookup_channel_team() covers the explicit-channel-ID path. Slack
   channels in format_directory_for_display are now grouped by
   workspace label, mirroring the Discord guild grouping.

2. tools/send_message_tool.py: _handle_send now uses the strict
   resolver and forwards team_id all the way to _send_slack. _send_slack
   reads ~/.hermes/slack_tokens.json to select the workspace-specific
   bot token, and fails loud with a 'Cross-channel' error if the
   workspace token is missing or Slack returns channel_not_found —
   never silently falling back to the primary token.

Tests:
- tests/gateway/test_channel_directory.py: 9 new tests in
  TestCrossWorkspaceSlackMisroute covering workspace tracking,
  ambiguity detection, strict resolver, explicit-ID team lookup, and
  workspace-grouped display.
- tests/tools/test_send_message_tool.py: 10 new tests covering
  workspace-specific token selection, fail-loud on missing workspace
  token, fail-loud on malformed tokens file, fail-loud on
  channel_not_found with team_id, and end-to-end ambiguity rejection
  at the tool-call layer.

Verified RED→GREEN via pytest tests/tools/test_send_message_tool.py
tests/gateway/test_channel_directory.py — 169 passed, 0 failed.

Co-authored-by: Claude <noreply@anthropic.com>
…te (NousResearch#32)

* fix(gateway): add OutboundGuard to prevent cross-channel Slack misroute

Production incident (2026-06-19 11:20:58–11:22:32 UTC):
  Inbound from C0AH3RY3DK6 (WorldArchitect) at 11:20:58.
  Orphan reply 1781868147.039389 posted to C0AJQ5M0A0Y (home channel)
  at 11:22:27 — 5 seconds BEFORE the correct reply to C0AH3RY3DK6.
  No thread_ts on the orphan because the inbound had no parent in C0AJQ5M0A0Y.

Root cause class: the gateway's outbound path used a chat_id that was
NOT derived from the inbound that triggered the response.

This change adds OutboundGuard (gateway/outbound_guard.py) — a
task-local contextvar that pins the inbound chat_id for the lifetime
of _handle_message_with_agent. Any adapter.send call whose chat_id
mismatches the active inbound is logged as a WARNING and recorded in
violations for the regression test.

Wiring:
  - Pin source.chat_id at handler entry (gateway/run.py:6943).
  - Reset in the existing finally block (gateway/run.py:7953) so the
    next handler's verify_send() checks are not contaminated.

Regression test (tests/hermes_cli/test_outbound_guard.py):
  - 8 tests cover pin/verify round-trip, exact incident repro pattern
    (pin A, send to B, send to A → exactly one violation), task-local
    isolation across concurrent asyncio tasks, allowed_extra_destinations
    opt-out for home-channel startup/shutdown notifications, and the
    unrestricted case where no chat_id is pinned.
  - All 8 tests pass.

* fix(gateway): wire OutboundGuard into real send paths; per-instance ContextVar; None-block

- outbound_guard.py: move _active_chat_id ContextVar into __post_init__
  so each OutboundGuard instance has its own per-instance ContextVar
  (regression for CR major on the dataclass mutable-default smell).
  Treat verify_send(None) while inbound is pinned as a violation
  rather than a silent bypass (regression for codex-connector P1 and
  CR major). Add module-level singleton _global_guard with
  pin_inbound/unpin_inbound/verify_outbound helpers so production call
  sites that don't hold a guard reference still see the handler's pin.
- run.py: route the per-handler pin through both the per-instance guard
  AND the module-level singleton; unpin both in the finally block.
- platforms/slack.py: SlackAdapter.send calls verify_outbound(chat_id)
  before chat_postMessage; returns a failed SendResult when misaligned.
- delivery.py: DeliveryRouter._deliver_to_platform calls
  verify_outbound(target.chat_id) before adapter.send; raises
  ValueError on misalignment (cron deliveries outside a handler still
  pass through because the guard has no pinned inbound).
- stream_consumer.py: replace every self.adapter.send call with a new
  _guarded_send helper that wraps verify_outbound around the send;
  on violation returns a fake failed SendResult without invoking the
  underlying adapter.
- tests/hermes_cli/test_outbound_guard.py: add test_none_chat_id_while_inbound_pinned_records_violation,
  test_module_level_singleton_pins_and_verifies,
  test_per_instance_contextvar_is_not_shared_between_guards,
  test_real_slack_send_with_aligned_chat_id_succeeds, and
  test_real_slack_send_with_misaligned_chat_id_is_refused.
  Apply the CR nitpicks: f-string assertion keys + shared guard
  instance in the task-locality test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(gateway): sentinel for no-active-inbound; guard all slack write paths

Three CR Major findings on the previous commit (234aafe6b1) addressed:

1. chat_id=None was bypassing the guard while inbound was pinned. The
   guard had no way to distinguish 'no handler active' from 'handler
   active with missing inbound chat_id' — both states collapsed to
   None via the Optional[str] ContextVar. Introduced _NO_ACTIVE_INBOUND
   sentinel object so verify_send can refuse sends through a handler
   that has no idea what channel to target (incident class includes
   no-destination sends when upstream code forgot to thread
   source.chat_id through).

2. The CR Major asked to extract the verify_outbound guard logic
   into a reusable helper and call it from every Slack write path.
   Added module-level _slack_guard_check() in slack.py and wired it
   into all 11 write methods: send, send_private_notice, _upload_file,
   send_multiple_images, send_image_file, send_image, send_voice,
   send_video, send_document, send_exec_approval, send_slash_confirm.

3. _send_draft_frame in stream_consumer.py uses adapter.send_draft
   (not adapter.send), so it slipped through the previous _guarded_send
   wiring. Added verify_outbound check at the top of _send_draft_frame
   that disables draft streaming on a misroute.

Tests: 13 existing tests pass + 3 new tests covering the sentinel:
  - test_pin_none_marks_handler_active_and_refuses_sends
  - test_pin_none_also_blocks_via_module_singleton
  - test_active_chat_id_distinguishes_inactive_from_pin_none

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(gateway): bound OutboundGuard.violations retention with deque(maxlen)

CR Major (review 4546711137) flagged the violations list as growing
unbounded for the lifetime of the _global_guard singleton — a
misbehaving handler spinning in a loop could exhaust process memory.

Replaced List[dict] with collections.deque(maxlen=MAX_VIOLATION_HISTORY=256).
Retains the most recent 256 violations for diagnostics while bounding
memory at a fixed cost. Added violation_count property for clean
test assertions (avoids deque/list equality noise).

Added 2 regression tests:
- test_violations_list_is_bounded — verifies retention cap
- test_violations_clear_works_with_bounded_deque — verifies clear_violations

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Replaces hardcoded Slack user/bot IDs with SLACK_LOOP_BLOCK_USERS,
SLACK_LOOP_BLOCK_BOTS, SLACK_LOOP_BLOCK_NAMES env vars.

Site-specific IDs move to launchd plist EnvironmentVariables.
Makes the feature generic and upstreamable.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…on write

_render_block() was injecting ALL entries from disk into the system prompt
with no truncation. memory_char_limit only guarded the add() write path, so
any file growth beyond the limit (direct disk writes, pre-limit migrations)
caused unbounded system prompt inflation.

Incident: MEMORY.md grew to 4,318 entries / 422KB → ~118k tokens injected
per session, exceeding provider context windows and causing silent failures.

Fix: _render_block() now keeps only the most-recent entries that fit within
memory_char_limit chars, dropping oldest-first. This mirrors the write guard
and is the correct invariant: the rendered block should never exceed the limit
regardless of how the file arrived at its current size.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ugin (#4)

* feat(plugins): pre_tool_call arg overrides + RTK command rewriting plugin

Squashed rebase of worktree_rtk_plugin onto origin/main.

- Add rewrite directive support to pre_tool_call hooks
- Add get_pre_tool_call_arg_overrides() for extracting rewrite directives
- Add coerce_tool_args() call after applying rewrite merges
- Add post-rewrite path conflict detection in concurrent execution
- Add RTK (Red Team Kit) command rewriting plugin
- Add comprehensive tests for directives and RTK plugin

* fix: address CR review findings on PR #4

- Remove duplicate AUTHOR_MAP key for jleechan2015 in scripts/release.py
- Add monkeypatch.delenv(HERMES_RTK_DISABLE) to success-path RTK tests

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
…ousResearch#11)

* feat(plugins): add rewrite directive support to pre_tool_call hooks

Introduces get_pre_tool_call_directives() which fires pre_tool_call once
and returns both block and rewrite directives, preserving the single-fire
contract.

Plugins can now return:
  {"action": "rewrite", "args": {new_args}}
to rewrite tool arguments before dispatch. First rewrite wins.
Existing block directives and observer-only hooks are unaffected.

Updates model_tools.py to use get_pre_tool_call_directives() so both
block and rewrite are checked in the same hook invocation.

Enables the RTK plugin to route terminal commands through `rtk rewrite`
for token savings without double-firing the hook.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Fix: Apply rewrite directives in primary agent loop paths

Replace get_pre_tool_call_block_message with get_pre_tool_call_directives in all three tool execution paths in run_agent.py to ensure rewrite directives from pre_tool_call hooks are properly applied before tool execution.

* fix(ci): add jleechan2015 to AUTHOR_MAP; suppress windows-footgun in process_registry

- scripts/release.py: map jleechan2015@users.noreply.github.com -> jleechan2015
- tools/process_registry.py: add windows-footgun suppression comment on
  os.killpg line; already guarded by _IS_WINDOWS check

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(test): e2e fixture bypass destructive slash confirm for /new tests

The e2e fixture now disables destructive_slash_confirm and provides
_agent_cache, _session_model_overrides, _queued_events so /new reaches
_handle_reset_command instead of blocking on the confirm gate.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(tests): mock Portal recommendation in nous auxiliary tests

Two tests were depending on the live Portal recommendation for the
Nous auxiliary model default. The Portal now returns qwen/qwen3.6-plus
instead of google/gemini-3-flash-preview, causing the assertions to
fail. Both tests now mock get_nous_recommended_aux_model to return
the expected model, making them independent of external state.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(test): provide mock runner with thread metadata helpers for streaming delivery tests

The 3 streaming delivery tests passed bare object() as the self argument
to GatewayRunner._deliver_media_from_response. The method calls
self._thread_metadata_for_source and self._reply_anchor_for_event,
raising AttributeError that was silently caught by the outer except block.
The mocks were never awaited because the delivery function exited early.

Replace object() with a SimpleNamespace that delegates to the real
thread-metadata helpers, and update metadata assertions to use the
actual return value instead of a hardcoded dict.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(test): update two pre-existing test failures

1. test_same_key_replaces_stale_loop_entry: cache key tuples now
   include an 8th pool_hint element. The test manually-constructed
   7-element key never matched the key computed by _client_cache_key,
   so the stale entry was never found and replaced. Added empty-string
   pool_hint to match production key format.

2. test_plugin_pre_tool_block_wins: production code was updated to use
   get_pre_tool_call_directives (returns block_message, rewrite_args
   tuple) but the test still patched the old get_pre_tool_call_block_message.
   The patch had no effect so handle_function_call ran normally. Updated
   to patch the correct function with the correct return shape.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(test): update block-message monkeypatches to get_pre_tool_call_directives

The pre-tool-call rewrite (commit 0af5641a3) replaced
get_pre_tool_call_block_message with get_pre_tool_call_directives
which returns (block_message, rewrite_args) instead of a plain
string. Four tests still monkeypatched the old function name, so
the block directive was never injected and the tools executed
normally, causing "should not run" assertion failures.

Update all four monkeypatch targets and return-value shapes:
- test_invoke_tool_blocked_returns_error_and_skips_execution
- test_invoke_tool_blocked_skips_handle_function_call
- test_sequential_blocked_tool_skips_checkpoints_and_callbacks
- test_blocked_memory_tool_does_not_reset_counter

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(test): add missing GatewayRunner attributes and use real plugin route

Two test failures:

1. test_update_streaming.py: _make_runner() created a bare GatewayRunner
   via object.__new__() but omitted .config, .session_store, and .hooks.
   _handle_message now accesses these in the pre_gateway_dispatch hook,
   _check_slash_access, and command hook emission paths. Add them:
   - config=None (policy_for_source returns disabled policy, all cmds pass)
   - session_store=None (hook kwarg, harmless when None)
   - hooks=HookRegistry() (empty registry, emit_collect returns [])

2. test_web_server.py: test_plugin_route_allows_auth referenced
   /api/plugins/example/hello from a nonexistent example-dashboard plugin.
   Replace with /api/plugins/kanban/board which auto-initialises its DB
   and returns 200 with valid auth.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(test): update find_gateway_pids mock output format

Tests used ps aux format (user/cpu columns) but the production code
now uses ps -A eww -o pid=,command= which expects "PID command" lines.
Also mock _get_service_pids to avoid launchd/systemd calls in tests.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(tests): mock /proc dir in gateway pid scan tests for CI

On Linux CI, /proc exists so find_gateway_pids reads /proc entries
instead of calling subprocess.run. Tests that mock subprocess.run
never fire, causing all 3 TestFindGatewayPidsExclude tests to fail.
Fix: monkeypatch os.path.isdir to return False for /proc, forcing
the ps-based code path.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(tests): mock aux-vision override in vision fast-path test

The test_vision_capable_main_model_uses_fast_path test was failing
because the CI config.yaml has an explicit auxiliary.vision.provider
set (wafer), which causes _explicit_aux_vision_override to return True,
short-circuiting to text mode and bypassing the native fast path.
Monkeypatch the override to return False so the fast-path gating
logic is properly exercised.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix pre-tool rewrite handling

* Fix PR11 uv resolver source

* Tighten pre-tool rewrite directive handling

* fix(pre-tool-rewrite): address review comments — checkpoints, dict guards, dedup

- run_agent.py: move concurrent-path checkpoints to after rewrite
  application so safety checks see the final (possibly rewritten)
  function_args, not the original args
- model_tools.py: add isinstance guards before dict unpacking in
  rewrite merge paths (lines 749-750 and 756-757) to prevent TypeError
  on non-dict args
- hermes_cli/plugins.py: replace get_pre_tool_call_block_message body
  with a thin wrapper delegating to get_pre_tool_call_directives,
  eliminating duplicate hook-firing and block-extraction logic
- pyproject.toml: add sha256 integrity hash to mistralai wheel URL
  for uv 0.11 pinned-source verification

Closes CodeRabbit: run_agent.py L10458 (block_message init), L10467
  (dict guard), model_tools.py L757 (dict guard), pyproject.toml L195
Closes Cursor: concurrent checkpoints before rewrite (Medium),
  duplicate block-check logic (Low)

* fix(pyproject): remove hash field from mistralai uv.sources entry

uv 0.11 does not support the `hash` field in [tool.uv.sources] URL
entries — it causes a TOML parse error blocking all CI jobs. Remove
the hash; the URL alone pins the exact wheel.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(pre-tool-rewrite): add isinstance guards to _invoke_tool rewrite paths

Guard both the `rewrite_args` merge (inside try/except) and the
`pre_tool_rewrite_args` fallback (elif branch outside try) with
isinstance checks to prevent TypeError on non-dict args, consistent
with all other merge sites in the concurrent and sequential paths.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
NousResearch#14)

* feat(plugins): add rewrite directive support to pre_tool_call hooks

Introduces get_pre_tool_call_directives() which fires pre_tool_call once
and returns both block and rewrite directives, preserving the single-fire
contract.

Plugins can now return:
  {"action": "rewrite", "args": {new_args}}
to rewrite tool arguments before dispatch. First rewrite wins.
Existing block directives and observer-only hooks are unaffected.

Updates model_tools.py to use get_pre_tool_call_directives() so both
block and rewrite are checked in the same hook invocation.

Enables the RTK plugin to route terminal commands through `rtk rewrite`
for token savings without double-firing the hook.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(test): e2e fixture bypass destructive slash confirm for /new tests

The e2e fixture now disables destructive_slash_confirm and provides
_agent_cache, _session_model_overrides, _queued_events so /new reaches
_handle_reset_command instead of blocking on the confirm gate.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(test): provide mock runner with thread metadata helpers for streaming delivery tests

The 3 streaming delivery tests passed bare object() as the self argument
to GatewayRunner._deliver_media_from_response. The method calls
self._thread_metadata_for_source and self._reply_anchor_for_event,
raising AttributeError that was silently caught by the outer except block.
The mocks were never awaited because the delivery function exited early.

Replace object() with a SimpleNamespace that delegates to the real
thread-metadata helpers, and update metadata assertions to use the
actual return value instead of a hardcoded dict.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(test): update find_gateway_pids mock output format

Tests used ps aux format (user/cpu columns) but the production code
now uses ps -A eww -o pid=,command= which expects "PID command" lines.
Also mock _get_service_pids to avoid launchd/systemd calls in tests.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(tests): mock /proc dir in gateway pid scan tests for CI

On Linux CI, /proc exists so find_gateway_pids reads /proc entries
instead of calling subprocess.run. Tests that mock subprocess.run
never fire, causing all 3 TestFindGatewayPidsExclude tests to fail.
Fix: monkeypatch os.path.isdir to return False for /proc, forcing
the ps-based code path.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Fix PR11 uv resolver source

* Tighten pre-tool rewrite directive handling

* fix(model_switch): strip Block Kit newline payload before parsing /model args

Slack appends Block Kit JSON after a double-newline to every message text.
parse_model_flags called .split() on all whitespace, so tokens from the
payload were joined with spaces, causing validate_requested_model to reject
the command with "Model names cannot contain spaces."

Fix: strip everything after the first newline at the top of parse_model_flags.
This is platform metadata, not user intent.

TDD: added tests/hermes_cli/test_parse_model_flags_block_kit.py (6 tests,
4 failed before fix, all 6 pass after).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(gateway): per-channel session cap + system load gate before AO spawn

- Guard A: per-channel concurrent session cap (MAX_SESSIONS_PER_CHANNEL=10)
  Uses _parse_session_key() to count active sessions by chat_id in _running_agents.
  Returns user-visible error when at cap instead of silently spawning.
- Guard B: system load gate via sysctl vm.loadavg (macOS) / /proc/loadavg (Linux)
  Declines spawn when loadavg_1m > 20.0; silently passes on measurement failure.
- Add top-level subprocess import (was only imported locally before)

Root cause: 2026-05-15 spawn storm — 517 sessions, loadavg=205, DNS starvation,
2h gateway outage. No concurrency cap existed in session spawn path.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* [agento] revert: remove per-channel cap + load gate from _handle_message

Both guards belong in AO kanban dispatch path (max_spawn=8 cap), not in
Hermes general message handler. Load gate blocked Hermes from responding
when AO workers raised system load. kanban.max_spawn=8 is the AO guard.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* [agento] fix: regenerate uv.lock — zipp source field corruption from rebase merge

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…plugins

- immediate_ack: posts text 'On it…' on inbound user-originated Slack messages
- reactions: places 👀 eyes reaction on processing start, swaps to ✅/❌ on session end
- Both are zero-edit to gateway/platforms/slack.py (per upstream-first policy)
- Both work with the new fork's plugin architecture (plugins/<name>/plugin.yaml)
- Pre-dispatch ack is the configured 'immediate_ack_text' from platforms.slack.extra
The previous check  was wrong — Slack
user IDs start with 'U' and bot IDs with 'B', but
is the AUTHOR's user (not the bot). For CLI-posted tests via
SLACK_USER_TOKEN the message has  set by the workspace
app config even though the author is the user. The correct check
is  (the raw field on the Slack event).

This ensures the plugin's pre-dispatch 'On it…' ack fires only
for true user-originated messages, not for bot-originated events
that happen to be sent via a user token.
…ontext API

CPython 3.14 refactored concurrent.futures.ThreadPoolExecutor to
pass a WorkerContext object to _worker instead of the legacy
(executor_ref, work_queue, initializer, initargs) tuple. The
_initializer and _initargs attributes were also removed from the
executor instance.

_DaemonThreadPoolExecutor._adjust_thread_count (in tools/async_delegation.py)
was still calling the 3.8–3.13 shape, which raises:

  'DaemonThreadPoolExecutor' object has no attribute '_initializer'

on Python 3.14+. That blocked every Hermes tool that used the daemon
pool (skill_view, memory-search, session_search, file/history
helpers, delegate_task fan-out) before doing any work.

Branch on the available API: when _create_worker_context exists
(3.14+), use the new WorkerContext path; otherwise fall back to the
legacy initializer/initargs tuple. Behavior matches stdlib on both
runtimes and preserves the daemon-worker exit semantics.

Verified locally on Python 3.14.4 — all 19 test_async_delegation.py
tests green, daemon worker roundtrip works.
@jleechan2015
jleechan2015 requested a review from a team July 24, 2026 17:12
@alt-glitch alt-glitch added invalid This doesn't seem right P3 Low — cosmetic, nice to have labels Jul 24, 2026

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for identifying the CPython 3.14 WorkerContext change. The underlying compatibility issue is valid, but this patch needs to be re-scoped before it can repair current main.

Problems

  • Current main extracted DaemonThreadPoolExecutor into tools/daemon_pool.py:37-64 in 3f2a56d1a4aab9511770b81ee595378440376ac0. tools/async_delegation.py:50-58 now only imports and aliases it, so this PR's tools/async_delegation.py:76-97 change does not affect the active implementation.
  • CPython 3.14 creates workers with (_executor_ref, _create_worker_context(), _work_queue) in Lib/concurrent/futures/thread.py:219-239; current tools/daemon_pool.py:55-60 still passes the removed _initializer and _initargs attributes.
  • The PR has 54 changed files, including unrelated workflow, gateway, and plugin changes. It also adds behavioral HERMES_REACTIONS* overrides in plugins/reactions/__init__.py:65-67,89-94, whereas repository policy places non-secret behavior in config.yaml.

Suggested changes

  • Salvage the WorkerContext branch into tools/daemon_pool.py and add a focused compatibility regression test beside tests/tools/test_daemon_pool.py.
  • Split all unrelated changes out of this compatibility fix.

This is an automated hermes-sweeper review.

Comment thread tools/async_delegation.py
# ``_worker`` consumes directly. 3.13 and earlier: ``_worker`` takes
# ``(executor_ref, work_queue, initializer, initargs)`` and the names
# are stored on the executor instance.
create_ctx = getattr(self, "_create_worker_context", None)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Current main extracted this executor into tools/daemon_pool.py in 3f2a56d1a4aab9511770b81ee595378440376ac0; async_delegation.py now only aliases it. Apply this compatibility branch to DaemonThreadPoolExecutor._adjust_thread_count in tools/daemon_pool.py, otherwise the current runtime path remains broken on Python 3.14.

"""
enabled = True
require_dm_or_mention = True
eyes = os.getenv("HERMES_REACTIONS_EYES", DEFAULT_EYES)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These new user-facing behavioral environment overrides conflict with the repository policy that non-secret settings belong in config.yaml. Keep the config.yaml keys and remove the HERMES_REACTIONS* override surface if this plugin is proposed separately.

@teknium1 teknium1 added sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-automation Sweeper risk: may affect CI, automerge, label sync, or maintainer automation sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:blast-broad Sweeper blast radius: broad — a core path most sessions hit labels Jul 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

invalid This doesn't seem right P3 Low — cosmetic, nice to have sweeper:blast-broad Sweeper blast radius: broad — a core path most sessions hit sweeper:risk-automation Sweeper risk: may affect CI, automerge, label sync, or maintainer automation sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants