fix(daemon_pool): support Python 3.14 ThreadPoolExecutor WorkerContext API - #70873
fix(daemon_pool): support Python 3.14 ThreadPoolExecutor WorkerContext API#70873jleechan2015 wants to merge 35 commits into
Conversation
…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.
…packages/cli/dist/index.js is missing
…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.
teknium1
left a comment
There was a problem hiding this comment.
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
DaemonThreadPoolExecutorintotools/daemon_pool.py:37-64in3f2a56d1a4aab9511770b81ee595378440376ac0.tools/async_delegation.py:50-58now only imports and aliases it, so this PR'stools/async_delegation.py:76-97change does not affect the active implementation. - CPython 3.14 creates workers with
(_executor_ref, _create_worker_context(), _work_queue)inLib/concurrent/futures/thread.py:219-239; currenttools/daemon_pool.py:55-60still passes the removed_initializerand_initargsattributes. - The PR has 54 changed files, including unrelated workflow, gateway, and plugin changes. It also adds behavioral
HERMES_REACTIONS*overrides inplugins/reactions/__init__.py:65-67,89-94, whereas repository policy places non-secret behavior inconfig.yaml.
Suggested changes
- Salvage the WorkerContext branch into
tools/daemon_pool.pyand add a focused compatibility regression test besidetests/tools/test_daemon_pool.py. - Split all unrelated changes out of this compatibility fix.
This is an automated hermes-sweeper review.
| # ``_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) |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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.
CPython 3.14 refactored ThreadPoolExecutor to pass WorkerContext to _worker. This PR makes _adjust_thread_count version-adaptive.