Skip to content
This repository was archived by the owner on Jun 4, 2026. It is now read-only.

⚠️ Sync inkbox with main (CONFLICTS) - #29

Merged
dimavrem22 merged 485 commits into
inkboxfrom
sync/upstream-main
May 20, 2026
Merged

⚠️ Sync inkbox with main (CONFLICTS)#29
dimavrem22 merged 485 commits into
inkboxfrom
sync/upstream-main

Conversation

@on-call-hermes-agent

@on-call-hermes-agent on-call-hermes-agent Bot commented May 17, 2026

Copy link
Copy Markdown

Upstream sync — CONFLICTS

Merging main (mirror of NousResearch/hermes-agent@main) into inkbox hit conflicts. Conflict markers are committed in the tree so the diff is reviewable.

To resolve: pull this branch locally, fix the marked files, and force-push.

git fetch origin sync/upstream-main
git checkout sync/upstream-main
# resolve conflicts in the files below
git add -A && git commit --amend --no-edit
git push --force origin sync/upstream-main

Conflicted files

README.md
gateway/platforms/webhook.py
gateway/run.py
hermes_cli/main.py
hermes_cli/send_cmd.py
run_agent.py
scripts/install.cmd
scripts/install.ps1
scripts/install.sh
scripts/release.py
tools/xai_http.py
uv.lock
website/docs/getting-started/installation.md

Opened by .github/workflows/sync-upstream.yml.

@github-actions

github-actions Bot commented May 17, 2026

Copy link
Copy Markdown

🔎 Lint report: sync/upstream-main vs origin/inkbox

ruff

Total: 0 on HEAD, 0 on base (➖ 0)

🆕 New issues: none

✅ Fixed issues: none

Unchanged: 0 pre-existing issues carried over.

ty (type checker)

Total: 9026 on HEAD, 8431 on base (🆕 +595)

🆕 New issues (483):

Rule Count
unresolved-attribute 299
invalid-argument-type 60
unresolved-import 51
invalid-assignment 43
invalid-parameter-default 11
unsupported-operator 4
unresolved-reference 3
invalid-type-form 3
unused-type-ignore-comment 3
unknown-argument 2
invalid-return-type 1
no-matching-overload 1
call-non-callable 1
not-subscriptable 1
First entries
run_agent.py:1282: [unresolved-attribute] unresolved-attribute: Object of type `Self@_flush_messages_to_session_db` has no attribute `session_id`
run_agent.py:1615: [unresolved-attribute] unresolved-attribute: Object of type `Self@interrupt` has no attribute `_execution_thread_id`
tests/tools/test_kanban_tools.py:1716: [unresolved-attribute] unresolved-attribute: Attribute `get` is not defined on `str` in union `str | dict[str, str | dict[str, dict[str, str]] | list[Unknown]] | dict[str, str | dict[str, dict[str, str] | dict[str, str | list[str]]] | list[Unknown]] | ... omitted 3 union elements`
tests/tools/test_managed_browserbase_and_modal.py:105: [invalid-assignment] invalid-assignment: Invalid subscript assignment with key of type `Literal["agent.browser_provider"]` and value of type `SimpleNamespace` on object of type `dict[str, ModuleType]`
run_agent.py:698: [unresolved-attribute] unresolved-attribute: Object of type `Self@_emit_status` has no attribute `status_callback`
hermes_cli/kanban.py:91: [invalid-return-type] invalid-return-type: Return type does not match returned value: expected `dict[str, str] | None`, found `dict[str, str | Any | None]`
run_agent.py:2093: [unresolved-attribute] unresolved-attribute: Object of type `Self@close` has no attribute `_active_children_lock`
tests/cron/test_cron_profile.py:153: [invalid-argument-type] invalid-argument-type: Method `__getitem__` of type `Overload[(key: SupportsIndex | slice[SupportsIndex | None, SupportsIndex | None, SupportsIndex | None], /) -> LiteralString, (key: SupportsIndex | slice[SupportsIndex | None, SupportsIndex | None, SupportsIndex | None], /) -> str]` cannot be called with key of type `Literal["properties"]` on object of type `str`
gateway/run.py:12119: [unresolved-attribute] unresolved-attribute: Object of type `AIAgent` has no attribute `context_compressor`
run_agent.py:875: [unresolved-attribute] unresolved-attribute: Object of type `Self@_resolved_api_call_timeout` has no attribute `model`
tests/gateway/test_telegram_channel_posts.py:35: [unresolved-attribute] unresolved-attribute: Unresolved attribute `MessageHandler` on type `ModuleType`
tests/gateway/test_telegram_callback_auth_fail_closed.py:33: [unresolved-attribute] unresolved-attribute: Unresolved attribute `HTTPXRequest` on type `ModuleType`
run_agent.py:1353: [unresolved-attribute] unresolved-attribute: Object of type `Self@_save_trajectory` has no attribute `model`
hermes_cli/main.py:9542: [invalid-argument-type] invalid-argument-type: Argument to function `describe_profile` is incorrect: Expected `str`, found `str | Any | None`
hermes_cli/main.py:9512: [invalid-argument-type] invalid-argument-type: Argument to function `normalize_profile_name` is incorrect: Expected `str`, found `Any | None`
tests/hermes_cli/test_kanban_swarm.py:36: [unresolved-attribute] unresolved-attribute: Attribute `assignee` is not defined on `None` in union `Task | None`
agent/agent_init.py:132: [invalid-parameter-default] invalid-parameter-default: Default value of type `None` is not assignable to annotated parameter type `IterationBudget`
gateway/run.py:16532: [invalid-assignment] invalid-assignment: Object of type `def _status_callback_sync(event_type: str, message: str) -> None` is not assignable to attribute `status_callback` on type `(Any & ~None) | AIAgent`
tests/gateway/test_agent_cache.py:464: [unresolved-attribute] unresolved-attribute: Object of type `AIAgent` has no attribute `step_callback`
gateway/run.py:16531: [invalid-assignment] invalid-assignment: Object of type `(def _interim_assistant_cb(text: str, *, already_streamed: bool = False) -> None) | None` is not assignable to attribute `interim_assistant_callback` on type `(Any & ~None) | AIAgent`
agent/auxiliary_client.py:2949: [invalid-argument-type] invalid-argument-type: Argument to function `resolve_provider_client` is incorrect: Expected `str`, found `None | (Any & ~AlwaysFalsy)`
tests/cli/test_update_command.py:23: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
tests/gateway/test_telegram_channel_posts.py:36: [unresolved-attribute] unresolved-attribute: Unresolved attribute `ContextTypes` on type `ModuleType`
run_agent.py:1575: [unresolved-attribute] unresolved-attribute: Object of type `Self@_save_session_log` has no attribute `session_log_file`
agent/agent_runtime_helpers.py:1530: [invalid-argument-type] invalid-argument-type: Argument to function `session_search` is incorrect: Expected `str`, found `Unknown | None`
... and 458 more

✅ Fixed issues (149):

Rule Count
invalid-argument-type 87
invalid-assignment 22
unresolved-attribute 22
unsupported-operator 6
unresolved-reference 5
not-subscriptable 3
unresolved-import 2
no-matching-overload 1
invalid-return-type 1
First entries
tests/agent/test_codex_cloudflare_headers.py:181: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `Literal["originator"]` and `(Unknown & ~AlwaysFalsy) | (str & ~AlwaysFalsy) | (dict[str, str] & ~AlwaysFalsy) | ... omitted 3 union elements`
run_agent.py:1713: [invalid-assignment] invalid-assignment: Invalid subscript assignment with key of type `Literal["timeout"]` and value of type `int | float` on object of type `dict[str, str | dict[str, str]]`
tests/hermes_cli/test_web_oauth_dispatch.py:26: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
hermes_cli/gateway.py:3719: [unresolved-reference] unresolved-reference: Name `logger` used when not defined
run_agent.py:1716: [invalid-assignment] invalid-assignment: Invalid subscript assignment with key of type `Literal["args"]` and value of type `list[str | Unknown]` on object of type `dict[str, str]`
run_agent.py:9305: [invalid-argument-type] invalid-argument-type: Argument to bound method `ContextCompressor.update_model` is incorrect: Expected `int`, found `str | Unknown | dict[Unknown | str, Unknown | str | dict[str, str]] | int | dict[Unknown, Unknown]`
run_agent.py:9088: [invalid-argument-type] invalid-argument-type: Argument to function `resolve_provider_client` is incorrect: Expected `str`, found `(Unknown & ~AlwaysFalsy) | (LiteralString & ~Literal[""]) | None | (str & ~AlwaysFalsy)`
cli.py:7773: [invalid-argument-type] invalid-argument-type: Argument to function `build_welcome_banner` is incorrect: Expected `int`, found `None | Unknown | int`
run_agent.py:2400: [invalid-argument-type] invalid-argument-type: Argument to `ContextCompressor.__init__` is incorrect: Expected `str`, found `None`
run_agent.py:8517: [unresolved-attribute] unresolved-attribute: Attribute `messages` is not defined on `None` in union `None | Unknown`
tests/hermes_cli/test_aux_config.py:50: [invalid-argument-type] invalid-argument-type: Method `__getitem__` of type `Overload[(key: SupportsIndex | slice[SupportsIndex | None, SupportsIndex | None, SupportsIndex | None], /) -> LiteralString, (key: SupportsIndex | slice[SupportsIndex | None, SupportsIndex | None, SupportsIndex | None], /) -> str]` cannot be called with key of type `Literal["max_concurrency"]` on object of type `str`
tests/run_agent/test_steer.py:193: [invalid-assignment] invalid-assignment: Object of type `None` is not assignable to attribute `_tool_worker_threads` of type `set[int]`
hermes_cli/kanban_db.py:4349: [invalid-argument-type] invalid-argument-type: Argument to function `_safe_int` is incorrect: Expected `str | None`, found `int`
acp_adapter/tools.py:1117: [unresolved-reference] unresolved-reference: Name `json` used when not defined
run_agent.py:11018: [invalid-argument-type] invalid-argument-type: Argument to function `memory_tool` is incorrect: Expected `str`, found `Unknown | None`
run_agent.py:9377: [invalid-assignment] invalid-assignment: Invalid assignment to data descriptor attribute `base_url` on type `Self@_try_recover_primary_transport` with custom `__set__` method
run_agent.py:15463: [invalid-argument-type] invalid-argument-type: Argument to bound method `AIAgent._strip_think_blocks` is incorrect: Expected `str`, found `Any | None`
run_agent.py:11653: [invalid-argument-type] invalid-argument-type: Argument to function `memory_tool` is incorrect: Expected `str`, found `Any | None`
run_agent.py:4409: [invalid-argument-type] invalid-argument-type: Argument to `AIAgent.__init__` is incorrect: Expected `str`, found `str | Unknown | dict[Unknown | str, Unknown | str | dict[str, str]] | int | dict[Unknown, Unknown]`
run_agent.py:8838: [invalid-assignment] invalid-assignment: Invalid subscript assignment with key of type `Literal["error"]` and value of type `InterruptedError` on object of type `dict[str, None | list[Unknown]]`
run_agent.py:2806: [invalid-argument-type] invalid-argument-type: Argument to function `get_model_context_length` is incorrect: Expected `str`, found `str | dict[str, str] | Any | ... omitted 3 union elements`
tests/agent/test_skill_commands.py:47: [unresolved-reference] unresolved-reference: Name `pytest` used when not defined
tests/hermes_cli/test_kanban_db.py:1441: [invalid-argument-type] invalid-argument-type: Argument to function `_safe_int` is incorrect: Expected `str | None`, found `Literal[1700000000]`
run_agent.py:13614: [invalid-argument-type] invalid-argument-type: Argument to bound method `SessionDB.update_token_counts` is incorrect: Expected `str | None`, found `str | Unknown | dict[Unknown | str, Unknown | str | dict[str, str]] | int | dict[Unknown, Unknown]`
run_agent.py:15395: [invalid-assignment] invalid-assignment: Object of type `None` is not assignable to `list[dict[str, Any]]`
... and 124 more

Unchanged: 4259 pre-existing issues carried over.

Diagnostics are surfaced as warnings — this check never fails the build.

HenkDz and others added 28 commits May 18, 2026 11:43
Extends NousResearch#26573 to also catch the case the original PR deliberately left
out: when a tool raises an exception, the agent's tool executor wraps it
in a canonical 'Error executing tool '<name>': ...' string prefix (see
agent/tool_executor.py around the try/except). That prefix is unique to
the wrapper and cannot legitimately appear in well-behaved tool output,
so it is a safe signal that the tool blew up.

Without this, the canonical 'tool raised' case still rendered as a green
'completed' row in Zed despite being a runtime failure — exactly the
class of bug NousResearch#26573 set out to fix.

Adds a positive test (raised-exception prefix -> failed) and a negative
test (bare 'Error:' word in legit tool output stays completed) so a
future contributor doesn't accidentally widen the rule to false-positive
on compiler/linter diagnostics.
Follow-up to NousResearch#26543. The sessions table does not have an updated_at
column (see hermes_state.py — only started_at/ended_at), so
row.get('updated_at') always returned None and the str() coercion was
dead code. Use datetime.now(UTC).isoformat() instead, which reflects
exactly what the field means here: 'the title was refreshed at this
moment'. Drop the dead coercion.
* feat(web): mobile dashboard UX polish

Bottom sheets for sidebar theme/language pickers on narrow viewports with
enter/exit animation and drag-to-close; inline header badges beside titles;
bottom padding on the route outlet for scroll clearance; profiles loading uses a
unicode braille spinner; align profile/cron card actions to the top; viewport-fit
cover and supporting layout tweaks across dashboard pages.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Fix Nix web npm hash and mobile sheet accessibility.

Align fetchNpmDeps in nix/web.nix with web/package-lock.json for CI. Improve BottomPickSheet backdrop labeling, avoid aria-hidden on the dialog during exit animation, and wire theme/language sheets with listbox semantics and localized dismiss labels.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
… ops

Three install.ps1 improvements pulled from the thin-installer work on
bb/gui (PR NousResearch#27822) that benefit the canonical CLI install flow on main:

1. Strip UTF-8 BOM from scripts/install.ps1.

   The canonical 'irm <raw URL> | iex' install flow has been broken
   since commit 4279da4 re-introduced a UTF-8 BOM that PR NousResearch#27224
   had explicitly stripped. PowerShell 5.1's 'irm' returns the
   response body as a string with the BOM surviving as a leading
   \ufeff character; 'iex' then evaluates that string and the parser
   chokes on the invisible character before param(), surfacing as a
   cascade of 'The assignment expression is not valid' errors at
   every param default value.

   File body is verified pure ASCII (no character above byte 127),
   so PS 5.1 with no BOM falls back to Windows-1252 decoding which
   is identical to ASCII for our content. Both install paths work:
     - 'irm ... | iex' (canonical one-liner)
     - 'powershell -File install.ps1' (programmatic / desktop bootstrap)

2. New -Commit and -Tag string params for reproducible pinning.

   Higher-precedence variants of -Branch. When set, the repository
   stage clones $Branch (fast partial fetch) and then 'git checkout's
   the exact ref. Precedence: Commit > Tag > Branch. Honoured by all
   three code paths:
     - Update path (existing valid checkout): fetch + checkout
       --detach <commit|tag> instead of checkout + pull.
     - Fresh clone: clone --branch $Branch, then post-clone
       'git checkout --detach' to the requested ref.
     - ZIP fallback: pick archive URL for the most-specific ref
       (commit -> archive/<sha>.zip, tag -> archive/refs/tags/
       <tag>.zip, else archive/refs/heads/<branch>.zip).

   Used by the Hermes desktop's first-launch bootstrap to pin the
   .exe to the exact commit it was built against, so the cloned
   Hermes Agent tree always matches what the .exe was tested with.
   Also enables release-bundle pinning (e.g. Microsoft Store builds
   pinning to a release tag) and CI reproducibility.

3. EAP=Continue wrap around the new pin-step git invocations.

   'git fetch origin <commit>' writes the routine 'From <url>' info
   line to stderr. Under the script's global $ErrorActionPreference
   = 'Stop' that stderr line is wrapped as an ErrorRecord and
   terminates the script even though fetch+checkout actually succeed.
   Same EAP=Stop + native-stderr footgun we hit during the install.ps1
   hardening pass in Install-Uv, Test-Python, _Run-NpmInstall.

   Wrap both the update-path fetch/checkout block AND the post-clone
   pin block in $ErrorActionPreference = 'Continue' (restored in
   finally). Real failures still caught by $LASTEXITCODE checks.
* feat: add /update slash command to CLI and TUI

* test(cli): add Python tests for /update slash command

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): address Copilot review for /update slash command

Route classic CLI /update through prompt_toolkit modal confirmation and
defer relaunch to the main-thread cleanup path after app.exit(). Tighten
Y/n semantics, add Python wrapper and catalog coverage tests, and assert
/update stays visible in the TUI command catalog.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): address review feedback on /update command

- Replace raw input() with _prompt_text_input_modal in _handle_update_command
  to avoid EOF/hang/keystroke-leak races with prompt_toolkit's stdin ownership
- Fix confirmation logic: only proceed on recognized affirmative aliases
  (y/yes/1/ok); cancel on everything else including empty string, typos,
  and unrecognized input — matches all other [Y/n] prompts in the codebase
- Route relaunch through main-thread shutdown path: set _pending_relaunch
  and return False from process_command so process_loop triggers app.exit();
  run() then calls relaunch() after prompt_toolkit has restored terminal modes
  and after cleanup — safe on both POSIX (execvp) and Windows (subprocess+exit)
- Fix misleading docstring in test_update_command.py: the Vitest only covers
  the TypeScript slash handler that emits code 42, not the Python wrapper
  branch that acts on it
- Rewrite tests to use SimpleNamespace pattern (like test_destructive_slash_confirm)
  so _prompt_text_input_modal can be stubbed directly
- Add Python test for _launch_tui exit-code-42 → relaunch branch in main.py

Agent-Logs-Url: https://github.com/NousResearch/hermes-agent/sessions/f6da68cf-e7b1-4b7a-aed6-3d4b0f523bdb

Co-authored-by: austinpickett <260188+austinpickett@users.noreply.github.com>

* fix(cli): polish test fixtures for /update command

- Remove unused _prompt_text_input from SimpleNamespace stub
- Use pytest.fail sentinel in managed-install guard test to catch unexpected modal invocations

Agent-Logs-Url: https://github.com/NousResearch/hermes-agent/sessions/f6da68cf-e7b1-4b7a-aed6-3d4b0f523bdb

Co-authored-by: austinpickett <260188+austinpickett@users.noreply.github.com>

* chore: re-trigger CI after Copilot review fixes

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: austinpickett <260188+austinpickett@users.noreply.github.com>
…1-improvements

feat(install.ps1): strip BOM, add -Commit/-Tag pin params, harden git ops
Adapts the upstream baoyu-article-illustrator skill (verbatim-copied in
the previous commit) to Hermes' tool ecosystem, matching the pattern
used by baoyu-infographic.

- Metadata: openclaw → hermes; add author, license, tags, category
- Triggering: slash command + CLI flags → natural language
- User config: remove EXTEND.md, first-time-setup, preferences-schema
- User prompts: AskUserQuestion (batched) → clarify (one at a time)
- Image gen: baoyu-imagine → image_generate (describe refs in prompt text)
- Platform: drop Windows/PowerShell; Linux/macOS only
- File ops: switch to write_file / read_file
- Watermark: opt-in per-article instead of EXTEND.md-driven
- Add PORT_NOTES.md describing the adaptation and sync procedure

Style, palette, and prompt/system.md reference files are verbatim copies
and are the sync points with upstream.
…ties

Addresses review feedback on NousResearch#13193:

1. Reference-image flow no longer assumes write_file/read_file handle
   binaries. vision_analyze produces a textual description; the binary
   is optionally copied via terminal (cp/curl). The description is what
   gets embedded in prompts.

2. image_generate's URL-only return is now explicit. Step 6 downloads
   the returned URL to local disk via terminal (curl -sSL -o ...), then
   verifies non-zero size before proceeding.

3. Removed "Please use nano banana pro..." line from prompts/system.md —
   the backend is user-configured and not agent-selectable, so routing
   hints in the prompt are misleading.

PORT_NOTES.md updated: prompts/system.md is no longer verbatim, and the
file-ops/backend-selection rows now reflect Hermes' actual tool surface
(write_file/read_file for text, terminal for binaries and URL downloads,
vision_analyze for reading images).
Adds the contributor email mapping for Jack Yang (@0xjackyang) so future
release-note generation attributes commits correctly.

Salvage of NousResearch#27964 by @0xjackyang.
Pre-stages AUTHOR_MAP entries for 5 new contributors whose PRs are being
salvaged in the May 2026 low-hanging-fruit batch (group 7). Lands ahead
of the per-PR salvage PRs so they don't get blocked by AUTHOR_MAP CI.

Contributors:
- 02356abc (NousResearch#28286 — wecom WSMsgType.CLOSING)
- burjorjee (NousResearch#28201 — inline-shell timeout guard)
- oseftg (NousResearch#28168 — natural response ending: emoji + caret)
- rudi193-cmd (NousResearch#28241 — empty credential pool entries)
- sadiksaifi (NousResearch#27982 — kanban horizontal scroll)

Per references/batch-pr-salvage-may14-additions.md.
The WeCom adapter's _read_events() loop only handled CLOSE, CLOSED,
and ERROR websocket message types. When the server initiates a graceful
shutdown, aiohttp returns WSMsgType.CLOSING before the connection is
fully closed. This message type was not handled, causing the receive()
call to return immediately in a tight loop while self._ws.closed
remained False. The result was 100% CPU usage on the asyncio event loop.

Add WSMsgType.CLOSING to the set of terminal message types that raise
RuntimeError("WeCom websocket closed"), allowing _listen_loop() to
enter its normal reconnect backoff path.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fixes NousResearch#28140

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
gateway.log uses a _ComponentFilter that only passes records from
loggers starting with ('gateway',). Plugin modules are loaded under
the hermes_plugins.* namespace, so all plugin log output is silently
dropped from gateway.log.

This makes plugin registration — which directly affects gateway hooks
(pre_gateway_dispatch, transform_llm_output, etc.) — invisible in
the gateway-specific log. Operators debugging gateway behavior check
gateway.log and see no plugin activity, even when plugins are working
correctly.

Add 'hermes_plugins' to the gateway component prefixes tuple so
plugin log messages appear in gateway.log.

Closes NousResearch#28138
_deliver_kanban_artifacts used a broader _IMAGE_EXTS that included
.bmp, .tiff, and .svg. These three extensions are absent from the
equivalent set in _deliver_media_from_response (line 10661), which
intentionally routes them through send_document rather than
send_multiple_images (comment near line 10522 notes that Telegram
sendPhoto recompresses and rejects non-raster formats).

Routing .svg (XML text), .bmp, or .tiff through the photo API causes
send_multiple_images to raise on most platforms; the exception is caught
and logged as a warning, silently dropping the artifact. Aligning the
two sets ensures kanban deliverables with these extensions follow the
same send_document path as regular agent responses.

No behaviour change for .png/.jpg/.jpeg/.gif/.webp.
…revent keyboard freeze

Background process non-PTY path used stdin=subprocess.PIPE unconditionally,
creating an orphan pipe that was never written to and never closed. Child
processes that read stdin would block indefinitely, competing with the
parent's prompt_toolkit event loop for terminal ownership and causing
complete keyboard lockout.

Change to stdin=subprocess.DEVNULL so children get immediate EOF on stdin
reads instead of blocking forever. For interactive stdin, the PTY path
(which has its own independent PTY via ptyprocess.PtyProcess.spawn) should
be used instead.

Fixes NousResearch#17959
…earch#28317)

* fix(process-registry): detach stdin from background subprocesses to prevent keyboard freeze

Background process non-PTY path used stdin=subprocess.PIPE unconditionally,
creating an orphan pipe that was never written to and never closed. Child
processes that read stdin would block indefinitely, competing with the
parent's prompt_toolkit event loop for terminal ownership and causing
complete keyboard lockout.

Change to stdin=subprocess.DEVNULL so children get immediate EOF on stdin
reads instead of blocking forever. For interactive stdin, the PTY path
(which has its own independent PTY via ptyprocess.PtyProcess.spawn) should
be used instead.

Fixes NousResearch#17959

* chore(release): alias stale-ID salvage commit for LifeJiggy

PR NousResearch#28315 was salvaged with a wrong noreply numeric ID (192385615 vs
the correct 141562589). The commit on main is correctly authored to
LifeJiggy by username, but the noreply email doesn't match AUTHOR_MAP.
Adds an alias so release-notes generation maps both forms to the same
contributor.

---------

Co-authored-by: LifeJiggy <192385615+LifeJiggy@users.noreply.github.com>
Plugin discovery exceptions in gateway startup (gateway/run.py) and
CLI startup (hermes_cli/main.py) are caught and logged at DEBUG
level, making them invisible at the default INFO log level.

If any plugin import fails — syntax error, missing dependency, import
cycle — operators get zero indication unless they bump the log level
to DEBUG. This makes broken plugins appear enabled but silently
non-functional.

Change both locations to logger.warning() so failures are visible at
production log levels.

Closes NousResearch#28137
… macOS

Path.resolve() follows the /tmp -> /private/tmp symlink on macOS, so
str(path).startswith("/tmp/") is always False for temp-dir paths.
The "Accept Edits" (workspace_session) mode silently refused to
auto-approve every /tmp write on macOS, breaking the documented
behaviour and making the existing test fail on this platform.

Fix: keep the raw expanded path (pre-resolve) for the /tmp prefix
check and continue using the resolved form only for the cwd
relative_to() call where symlink resolution is correct behaviour.
Switch .hermes-kanban-columns from auto-fit CSS grid to a flex row with
overflow-x: auto and a hidden scrollbar (scrollbar-width / ::-webkit-
scrollbar), and pin .hermes-kanban-column to flex: 0 0 280px so columns
sit side-by-side at a fixed width instead of wrapping into a 2xN grid.

Page vertical scroll is unaffected: each column already caps at
max-height: calc(100vh - 220px), so the container never grows tall
enough to introduce its own vertical scrollbar.
…sible to LLM

When a tool call requires user approval in the non-blocking gateway path,
the LLM previously received a result that was indistinguishable from a
failed tool call (exit_code=-1, error=message). The LLM could not tell
whether the tool was pending approval, had returned empty results, or had
failed silently — causing it to burn context on wrong hypotheses.

Fix changes the result format to include:
- status: pending_approval (clear state name)
- approval_pending: True (explicit boolean for LLMs to detect)
- error: cleared to empty string (removes misleading error signal)

This lets the LLM reason about approval latency vs actual errors,
short-circuiting the previous silent failure mode.

Fixes NousResearch#14806
teknium1 and others added 23 commits May 18, 2026 23:59
…vent data loss (NousResearch#27856)

Pre-mark all running agent sessions as resume_pending BEFORE the drain
wait begins. If the service manager kills the process during the drain
(window), the durable marker is already written so the next gateway boot
can recover in-flight sessions. On graceful drain completion, clear the
early markers for sessions that finished successfully.
…reply loops

In multi-agent shared Matrix rooms, multiple bots all participating in the
same thread could trigger infinite reply loops — each bot's reply re-engaged
the others because they were all in the bot-thread set. Discord has a
`thread_require_mention` opt-in for this; Matrix didn't.

Add `_parse_thread_require_mention(config)` (mirrors Discord's pattern).
In `_resolve_message_context`, when enabled and the message is in a
bot-participated thread (not a free-response room), require @mention
before processing.

Salvage of @justemu's 2-commit stack (NousResearch#27996). Fixes NousResearch#27995.
…e_file

Adds TestGitBaselineCheck with 6 unit tests covering _check_git_baseline
and the warning field in write_file result:
- Git not available → None
- Not in a git repo → None
- Clean repo → None
- Dirty repo → returns warning string with branch name
- write_file result includes warning when dirty
- write_file result omits warning when clean
HERMES_TUI_RESUME is an internal env var the Python wrapper exports to hand
a session ID off to the Ink TUI. Because _launch_tui started from
os.environ.copy(), any exported/stale value in the user's shell leaked
through — so plain `hermes --tui` would try to resume a missing session
and leave the UI at 'error: session not found' with no live session.

Drop HERMES_TUI_RESUME from the env before conditionally re-setting it
from the argparse-resolved resume_session_id. Tests cover both the drop
path and the set-from-arg path.

Salvage of NousResearch#28080 by @noctilust.
1. trajectory_compressor.py: yaml.safe_load() returns None on empty
   files, crashing with TypeError on `if 'tokenizer' in data`. Fix by
   adding `or {}` fallback. (HIGH — blocks startup with empty config)

2. 6 files with fcntl.flock(LOCK_UN) in finally blocks without
   try/except: cron/scheduler.py, hermes_cli/auth.py,
   agent/shell_hooks.py, tools/skill_usage.py,
   tools/environments/file_sync.py, tools/memory_tool.py. If unlock
   raises OSError, fd.close() is skipped and the lock is held forever.
   The msvcrt branches already had try/except; the fcntl branches did
   not. Fix by wrapping in try/except (OSError, IOError): pass.

3. agent/copilot_acp_client.py line 639: TOCTOU race — path.exists()
   followed by path.read_text() with no try/except. If file is deleted
   between the check and the read, FileNotFoundError propagates. Fix
   by using try/except FileNotFoundError.

4. gateway/sticker_cache.py: non-atomic write via Path.write_text()
   can leave truncated JSON on crash, causing JSONDecodeError on next
   load. Fix by writing to tempfile + fsync + os.replace (atomic).
…salvage (NousResearch#28594)

Adds the canonical noreply form (54813621+xxxigm@users.noreply.github.com)
alongside the existing plain-email mapping so the salvage commit for
@xxxigm's codex doctor PR doesn't fail AUTHOR_MAP CI.
…ousResearch#27975

`hermes doctor` printed 'codex CLI not installed (optional — ...)' as a
generic info line at the bottom of the auth section, several rows below
'OpenAI Codex auth (not logged in)' and after MiniMax/Gemini auth checks.
Users reading sequentially mistook it for MiniMax-related advice.

Move the hint up under the Codex auth warning so it's adjacent to the
row it actually pertains to. Behavior unchanged when the codex CLI is
installed (success path keeps its 'codex CLI ✓' row at the bottom).
Tests cover both placement and suppression cases.

Salvage of @xxxigm's 3-commit stack (NousResearch#27986).
Closes NousResearch#27975.
…#28626)

Sweep of all CI failures on origin/main, grouped by drift source:

Telegram allowlist gate (db50af9 added user-authz to _should_process_message):
- Hardcoded "[Telegram]" prefix in the logger.warning so the call no
  longer dereferences self.name → self.platform, which test fixtures
  built via object.__new__ never set.
- test_telegram_format / test_allowed_channels_widening fixtures stub
  _is_callback_user_authorized → True so the new gate doesn't reject
  guest-mode / allowed-channels test messages.
- test_telegram_approval_buttons::test_update_prompt_callback_not_affected
  sets TELEGRAM_ALLOWED_USERS="*" so the fail-closed default doesn't
  reject the callback before it writes .update_response.

Approval surface (6d495d9 renamed status, 214b953 detached stdin):
- test_no_callback_returns_approval_required: status is now
  "pending_approval" (was "approval_required").
- test_close_stdin_allows_eof_driven_process_to_finish: switch to
  use_pty=True; non-PTY now uses stdin=DEVNULL.

Mattermost (send() now resolves root_id via _api_get first):
- test_send_with_thread_reply mocks _session.get with a thread-root
  response so the new resolver doesn't TypeError on a bare AsyncMock.

Kanban (d8ad431 rename, f55d94a review column, _kanban_worker_skill_available):
- _safe_int → _to_epoch in the two test_kanban_db tests.
- Spawn-skills tests (×3) monkey-patch _kanban_worker_skill_available
  to True since the isolated kanban_home fixture has no devops/kanban-worker tree.
- test_gateway_dispatcher_disables_corrupt_board: connect count
  3 → 5 (review-column probe now also runs per tick).

Aux-config severity at_or_above (a94ddd8):
- test_diagnostics_endpoint_severity_filter expects warning filter to
  include error+critical now (was exact-match).

Anthropic error handling (conversation loop extracted from run_agent):
- _no_backoff_wait fixture patches BOTH run_agent.jittered_backoff AND
  agent.conversation_loop.jittered_backoff. The latter is the actual
  call site; without the second patch tests burn ~2s per retry and
  hit the 30s SIGALRM timeout on CI.

Other test pollution / drift:
- test_auto_does_not_select_copilot_from_github_token: patch
  agent.bedrock_adapter.has_aws_credentials → False so boto3's
  credential chain can't auto-pick Bedrock from developer ~/.aws.
- test_setup_openclaw_migration: patch hermes_cli.gateway.get_env_value
  in addition to setup_mod.get_env_value — _platform_status reads
  through the gateway module's binding.
- test_gateway_prefix: COMPONENT_PREFIXES["gateway"] now includes
  "hermes_plugins" too.
- test_recommended_update_command_defaults_to_hermes_update: also
  short-circuit get_managed_update_command in case a stray
  ~/.hermes/.managed marker is present.
- test_user_id_is_not_explicit: _parse_target_ref now returns
  is_explicit=False for Slack U.../W... IDs (chat.postMessage rejects
  them — a DM must be opened first via conversations.open).
… on failure (NousResearch#28669)

Catch the PR NousResearch#28452 failure mode (orphan merge-conflict markers in
hermes_cli/config.py) on the user side: after git pull succeeds, compile
the files every 'hermes' invocation imports at startup. If any has a
syntax error, git reset --hard back to the pre-pull SHA so the install
stays bootable. User can retry once a fix lands upstream.

- New _capture_head_sha() + _validate_critical_files_syntax() helpers
- Wires both into _cmd_update_impl after the pull/reset succeeds
- Tests cover the helpers, the rollback flow, and a production-tree
  invariant (CI fails if main itself has a syntax error in a critical
  file — catches future broken commits before users hit them)
When 'hermes update' syncs bundled skills, the summary line only shows
the count of user-modified skills that were kept (e.g. '3 user-modified
(kept)'), but not *which* skills. Once the update finishes, the user
has no way to know which skills need triage.

Append the skill names to the summary line, truncated to 5 with a
'+N more' suffix for long lists:

  Done: 12 new, 3 updated, 7 unchanged, 3 user-modified (kept):
  hermes-agent, debugging-hermes-tui-commands, system-health.
  25 total bundled.

Closes NousResearch#28121
NousResearch#28063 fixed the macOS `/tmp`→`/private/tmp` symlink issue by checking
the RAW path (pre-resolve) against startswith('/tmp/'). That works on
Linux + macOS but not on Windows — Path('/tmp/foo').resolve() returns
C:\\tmp\\foo and isn't the real Windows temp anyway.

Replace the hardcoded '/tmp/' prefix with Path(tempfile.gettempdir()).
resolve() + Path.relative_to() — same idiom as the cwd branch just
below. Works correctly on Linux (/tmp), macOS (/private/var/folders/...),
and Windows (%LOCALAPPDATA%\\Temp).

Test rewritten to use tempfile.gettempdir() so the assertion exercises
the same code path on every platform.

Conflict against the just-merged NousResearch#28063 (raw_path approach) resolved
by replacing the whole raw_path block — tempfile.gettempdir() is
strictly better than that intermediate fix.

Salvage of NousResearch#28262 by @Zyrixtrex.
…h#28680)

Follow-up to NousResearch#28452. detect_stale_running() was calling
_record_task_failure() on every reclaim, which ticked the
consecutive_failures counter. With the default failure_limit=2,
two legitimately long-running tasks (>4 h without explicit
heartbeat) would auto-block via the spawn-failure circuit
breaker — even though no worker actually failed.

Stale reclaim is dispatcher-side absence-of-heartbeat detection,
not a worker fault. Removed the _record_task_failure() call;
the 'stale' event in task_events is still the audit surface,
but the failure counter is now reserved for spawn_failed /
timed_out / crashed (real failures).

Also documents the heartbeat requirement:
- KANBAN_GUIDANCE in agent/prompt_builder.py now states the
  rule ('call kanban_heartbeat at least once an hour for tasks
  running longer than 1 hour') so workers learn the contract.
- kanban.md adds the stale event row to the events table and
  flags the heartbeat requirement in the worker lifecycle list.

New regression test: test_detect_stale_does_not_tick_failure_counter
locks in the new behaviour.
NousResearch#28672, NousResearch#28674, NousResearch#28676, NousResearch#28678)

Five small fixes against issues filed during the post-merge salvage audit:

* NousResearch#28670: `_GATEWAY_PROVIDER_ERROR_RE` false-positives on legitimate prose.
  Replace the regex with an anchored `_GATEWAY_PROVIDER_ERROR_SHAPE_RE` and
  add a length-cap heuristic to `_looks_like_gateway_provider_error`:
  short envelope at the start of the message → real provider error; long
  prose containing 'HTTP 404' → assistant answer, leave alone.

* NousResearch#28672: drop the pointless 1s asyncio.sleep on Telegram thread-not-found
  retries. The same-thread retry is preserved (catches Telegram's
  occasional transient flake exercised by
  test_send_retries_transient_thread_not_found_before_fallback) but with
  no artificial delay.

* NousResearch#28674: broaden `_should_retry_without_dm_topic_reply_anchor` to also
  fire when Bot API rejects `direct_messages_topic_id` for synthetic /
  resumed sends that have no reply anchor. Avoids dropping post-resume
  background notifications if the topic id goes stale.

* NousResearch#28676: delete the dead image-document branch superseded by bd0c54d
  (which returns early on the same extension set).

* NousResearch#28678: extend chat-scoped allowlist (`TELEGRAM_GROUP_ALLOWED_CHATS`)
  to also cover `chat_type == 'channel'`, so operators can authorize
  channel posts by chat id without falling back to per-user allowlists.

Tests:
- scripts/run_tests.sh tests/gateway/test_telegram_thread_fallback.py -q  → 41/41
- scripts/run_tests.sh tests/cron/test_scheduler.py -q                    → 127/127
- broader test set: same 3 pre-existing test-pollution failures reproduce
  on plain main.
@github-actions

Copy link
Copy Markdown

🚨 CRITICAL Supply Chain Risk Detected

This PR contains a pattern that has been used in real supply chain attacks. A maintainer must review the flagged code carefully before merging.

🚨 CRITICAL: Install-hook file added or modified

These files can execute code during package installation or interpreter startup.

Files:

setup.py
skills/productivity/google-workspace/scripts/setup.py

Scanner only fires on high-signal indicators: .pth files, base64+exec/eval combos, subprocess with encoded commands, or install-hook files. Low-signal warnings were removed intentionally — if you're seeing this comment, the finding is worth inspecting.

Conflicted files (markers in tree, resolve manually):

README.md
gateway/platforms/webhook.py
gateway/run.py
hermes_cli/main.py
hermes_cli/send_cmd.py
run_agent.py
scripts/install.cmd
scripts/install.ps1
scripts/install.sh
scripts/release.py
tools/xai_http.py
uv.lock
website/docs/getting-started/installation.md
@dimavrem22
dimavrem22 force-pushed the sync/upstream-main branch from 1ce88cd to 5d42a44 Compare May 20, 2026 04:11
@github-actions

Copy link
Copy Markdown

🚨 CRITICAL Supply Chain Risk Detected

This PR contains a pattern that has been used in real supply chain attacks. A maintainer must review the flagged code carefully before merging.

🚨 CRITICAL: Install-hook file added or modified

These files can execute code during package installation or interpreter startup.

Files:

setup.py
skills/productivity/google-workspace/scripts/setup.py

Scanner only fires on high-signal indicators: .pth files, base64+exec/eval combos, subprocess with encoded commands, or install-hook files. Low-signal warnings were removed intentionally — if you're seeing this comment, the finding is worth inspecting.

@dimavrem22
dimavrem22 merged commit ab97c86 into inkbox May 20, 2026
11 of 13 checks passed
on-call-hermes-agent Bot pushed a commit that referenced this pull request May 27, 2026
Upstream commit 7c622b6 ("fix(kanban): migrate task session index after
columns") removed three CREATE INDEX statements from SCHEMA_SQL and
re-added the corresponding indexes inside _migrate_add_optional_columns,
after the columns themselves are added. The legacy-board migration was
failing because CREATE TABLE IF NOT EXISTS does not add new columns to
existing tables, so the SCHEMA_SQL-level CREATE INDEX ran against a
table that didn't yet have the indexed column.

Two prior github-actions[bot] sync PRs (#29, #33) left the inkbox branch
with the migration half of the fix applied (the new indexes are created
after columns) but missed deleting one line from SCHEMA_SQL —
specifically `CREATE INDEX IF NOT EXISTS idx_tasks_session_id ON
tasks(session_id);`. That kept the bug live: the test
`test_connect_migrates_legacy_db_before_optional_column_indexes` has
been failing on the inkbox branch since at least PR #39, and the
running gateway throws `sqlite3.OperationalError: no such column:
session_id` from the kanban dispatcher every minute.

Surgical: delete the orphan line. After this commit, kanban_db.py is
byte-identical to upstream/main. Test passes locally.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.