Skip to content

fix(telegram): expose group chat ID in context prompt for media routing - #1

Closed
elphamale wants to merge 0 commit into
mainfrom
feat/telegram-group-media-routing
Closed

fix(telegram): expose group chat ID in context prompt for media routing#1
elphamale wants to merge 0 commit into
mainfrom
feat/telegram-group-media-routing

Conversation

@elphamale

Copy link
Copy Markdown
Owner

Problem

When the bot is @mentioned in a Telegram group, build_session_context_prompt only shows the group by name ("group: Pan Intended") — no numeric chat ID. The agent sees the home channel's ID listed explicitly ("Home Channels: telegram: Aineko (ID: 123456789)") and picks that for send_message(\"telegram:123456789\", ...), routing media to the owner's private channel instead of the group.

Text responses route correctly because they use the answerGuestQuery path or the Telegram platform adapter's reply routing — but send_message tool calls with explicit telegram:ID bypass the session-context fallback added in Bug 2 fix.

Fix

Add a Telegram-specific IDs block for group/channel sources (matching the existing Discord and Matrix pattern). When the source is a group or channel, the context prompt now includes:

**Telegram current chat:** Pan Intended (ID: `-1001234567890`)
  - To deliver files/media **into this group**, use `send_message('telegram:-1001234567890', ...)`. Prefer this over the home channel when the user made the request here.

This gives the agent a directly-usable target for the current chat, so it can choose the group over the home channel when responding to an @mention.

Test

  1. @mention the bot in a Telegram group and request a music download
  2. Verify the file arrives in the group chat, not the home/private channel

🤖 Generated with Claude Code

@github-actions

Copy link
Copy Markdown

⚠️ MCP catalog security review required

This PR changes the bundled MCP catalog or MCP catalog installer code. MCP entries can define local commands that users later install into mcp_servers, so this needs explicit maintainer review before merge.

A maintainer should verify:

  • any new/changed optional-mcps/**/manifest.yaml command and args are expected,
  • stdio transports do not use shell+egress/exfiltration payloads,
  • git install refs are pinned and bootstrap commands are minimal,
  • requested env vars/secrets match the upstream MCP's documented needs.

After review, add the mcp-catalog-reviewed label and re-run this check.

@github-actions

github-actions Bot commented Jun 17, 2026

Copy link
Copy Markdown

🔎 Lint report: feat/telegram-group-media-routing vs origin/main

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: 10995 on HEAD, 10678 on base (🆕 +317)

🆕 New issues (291):

Rule Count
invalid-argument-type 66
unresolved-attribute 61
unresolved-import 60
invalid-assignment 51
unsupported-operator 31
not-subscriptable 6
invalid-method-override 5
no-matching-overload 4
invalid-return-type 4
unresolved-reference 2
unused-awaitable 1
First entries
gateway/slash_commands.py:1836: [invalid-argument-type] invalid-argument-type: Argument to function `_home_thread_env_var` is incorrect: Expected `str`, found `Literal["local", "telegram", "discord", "whatsapp", "whatsapp_cloud", ... omitted 18 literals] | set[Unknown]`
tests/plugins/platforms/photon/test_reactions.py:13: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
tests/agent/test_compress_focus.py:147: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `Literal["- third"]` and `Unknown | None`
tests/agent/test_auxiliary_config_bridge.py:285: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `Literal["model"]` and `Unknown | int | str | ... omitted 15 union elements`
gateway/slash_commands.py:1835: [invalid-argument-type] invalid-argument-type: Argument to function `_home_target_env_var` is incorrect: Expected `str`, found `Literal["local", "telegram", "discord", "whatsapp", "whatsapp_cloud", ... omitted 18 literals] | set[Unknown]`
tests/cron/test_suggestions.py:198: [invalid-argument-type] invalid-argument-type: Method `__getitem__` of type `Overload[(i: SupportsIndex, /) -> Unknown, (s: slice[SupportsIndex | None, SupportsIndex | None, SupportsIndex | None], /) -> list[Unknown]]` cannot be called with key of type `Literal["monitor"]` on object of type `list[Unknown]`
tools/delegate_tool.py:2307: [invalid-argument-type] invalid-argument-type: Argument to function `dispatch_async_delegation` is incorrect: Expected `str | None`, found `Any | None | str | list[str]`
tests/agent/test_custom_pool_mismatch_guard.py:18: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
hermes_cli/cli_commands_mixin.py:1332: [unresolved-attribute] unresolved-attribute: Object of type `Self@_handle_blueprint_command` has no attribute `_console_print`
tests/gateway/test_matrix_project_context_isolation.py:49: [invalid-assignment] invalid-assignment: Object of type `MagicMock` is not assignable to attribute `_background_read_receipt` of type `def _background_read_receipt(self, room_id: str, event_id: str) -> None`
gateway/run.py:14924: [invalid-assignment] invalid-assignment: Object of type `str` is not assignable to attribute `memory_notifications` on type `(Any & ~None) | AIAgent`
hermes_cli/config.py:4779: [unresolved-attribute] unresolved-attribute: Attribute `items` is not defined on `str`, `list[Unknown]`, `list[str]`, `None`, `int` in union `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 33 union elements`
tests/gateway/test_restart_resume_pending.py:1653: [invalid-assignment] invalid-assignment: Object of type `bound method GatewayRunner._release_running_agent_state(session_key: str, *, run_generation: int | None = None) -> bool` is not assignable to attribute `_release_running_agent_state` of type `def _release_running_agent_state(self, session_key: str, *, run_generation: int | None = None) -> bool`
tests/hermes_cli/test_whatsapp_cloud_setup.py:212: [invalid-argument-type] invalid-argument-type: Argument to function `len` is incorrect: Expected `Sized`, found `str | None`
hermes_cli/config.py:4905: [invalid-assignment] invalid-assignment: Object of type `None` is not assignable to `def validate_mcp_server_entry(name: str, entry: dict[str, Any]) -> list[str]`
tests/gateway/test_gateway_silence_tokens.py:50: [invalid-assignment] invalid-assignment: Object of type `(_event) -> None` is not assignable to attribute `_get_guild_id` of type `def _get_guild_id(event: MessageEvent) -> int | None`
cron/jobs.py:26: [invalid-assignment] invalid-assignment: Object of type `None` is not assignable to `<module 'fcntl'>`
tests/gateway/test_restart_resume_pending.py:1679: [invalid-assignment] invalid-assignment: Object of type `AsyncMock` is not assignable to attribute `_keep_typing` of type `def _keep_typing(self, chat_id: str, interval: int | float = ..., metadata=None, stop_event: Event | None = None) -> CoroutineType[Any, Any, None]`
gateway/slash_commands.py:2970: [invalid-argument-type] invalid-argument-type: Argument to function `query_session_listing` is incorrect: Expected `str | None`, found `Literal["local", "telegram", "discord", "whatsapp", "whatsapp_cloud", ... omitted 18 literals] | set[Unknown]`
gateway/session.py:257: [invalid-argument-type] invalid-argument-type: Argument to bound method `PlatformRegistry.get` is incorrect: Expected `str`, found `Literal["local", "telegram", "discord", "whatsapp", "whatsapp_cloud", ... omitted 18 literals] | set[Unknown]`
tests/run_agent/test_codex_silent_hang_hint.py:30: [invalid-argument-type] invalid-argument-type: Argument to `AIAgent.__init__` is incorrect: Expected `((str, dict[Unknown, Unknown], /) -> None) | None`, found `str | bool`
tests/gateway/test_gateway_silence_tokens.py:49: [invalid-assignment] invalid-assignment: Object of type `(_event) -> None` is not assignable to attribute `_reply_anchor_for_event` of type `def _reply_anchor_for_event(event: MessageEvent) -> str | None`
tests/cron/test_suggestions.py:198: [not-subscriptable] not-subscriptable: Cannot subscript object of type `None` with no `__getitem__` method
gateway/platforms/whatsapp_cloud.py:1415: [unresolved-attribute] unresolved-attribute: Attribute `Response` is not defined on `None` in union `Unknown | None`
tests/gateway/test_restart_resume_pending.py:1659: [invalid-assignment] invalid-assignment: Object of type `(...) -> Literal[0]` is not assignable to attribute `_invalidate_session_run_generation` of type `def _invalidate_session_run_generation(self, session_key: str, *, reason: str = "") -> int`
... and 266 more

✅ Fixed issues (84):

Rule Count
invalid-argument-type 33
unresolved-attribute 16
unsupported-operator 14
invalid-assignment 9
unresolved-import 6
invalid-return-type 2
unknown-argument 2
invalid-method-override 2
First entries
tests/cli/test_cli_status_bar.py:162: [unresolved-import] unresolved-import: Cannot resolve imported module `prompt_toolkit.application`
gateway/platforms/base.py:2179: [unresolved-attribute] unresolved-attribute: Attribute `title` is not defined on `set[Unknown]` in union `Literal["local", "telegram", "discord", "whatsapp", "slack", ... omitted 17 literals] | set[Unknown]`
gateway/slash_commands.py:1692: [invalid-argument-type] invalid-argument-type: Argument to function `_home_thread_env_var` is incorrect: Expected `str`, found `Literal["local", "telegram", "discord", "whatsapp", "slack", ... omitted 17 literals] | set[Unknown]`
gateway/run.py:1634: [invalid-return-type] invalid-return-type: Return type does not match returned value: expected `str`, found `Literal["cli", "telegram", "discord", "whatsapp", "slack", ... omitted 17 literals] | set[Unknown]`
hermes_cli/web_server.py:2449: [invalid-argument-type] invalid-argument-type: Argument to function `add_lineage_result` is incorrect: Expected `str`, found `Any | None`
gateway/run.py:5669: [invalid-argument-type] invalid-argument-type: Argument to bound method `GatewayRunner._update_platform_runtime_status` is incorrect: Expected `str`, found `Literal["local", "telegram", "discord", "whatsapp", "slack", ... omitted 17 literals] | set[Unknown]`
gateway/run.py:11428: [invalid-argument-type] invalid-argument-type: Argument to function `set_session_vars` is incorrect: Expected `str`, found `Literal["local", "telegram", "discord", "whatsapp", "slack", ... omitted 17 literals] | set[Unknown]`
hermes_cli/web_server.py:3586: [invalid-argument-type] invalid-argument-type: Argument to bound method `set.add` is incorrect: Expected `str`, found `Literal["telegram", "discord", "whatsapp", "slack", "signal", ... omitted 16 literals] | set[Unknown]`
tests/cli/test_resume_display.py:716: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `Literal["resume_display"]` and `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 31 union elements`
tools/file_operations.py:260: [invalid-assignment] invalid-assignment: Invalid subscript assignment with key of type `Literal["error"]` and value of type `str & ~AlwaysFalsy` on object of type `dict[str, int]`
hermes_cli/config.py:4727: [unresolved-attribute] unresolved-attribute: Attribute `items` is not defined on `int`, `str`, `list[Unknown]`, `float`, `None` in union `Unknown | int | str | ... omitted 14 union elements`
gateway/run.py:13764: [invalid-argument-type] invalid-argument-type: Argument to function `resolve_display_setting` is incorrect: Expected `str`, found `Literal["cli", "telegram", "discord", "whatsapp", "slack", ... omitted 17 literals] | set[Unknown]`
gateway/platforms/yuanbao.py:2790: [invalid-argument-type] invalid-argument-type: Argument is incorrect: Expected `MessageType`, found `Literal[MessageType.DOCUMENT] | Any | None`
hermes_cli/web_server.py:8965: [unresolved-attribute] unresolved-attribute: Attribute `execute` is not defined on `None` in union `None | Connection`
gateway/slash_commands.py:633: [unresolved-attribute] unresolved-attribute: Attribute `lower` is not defined on `set[Unknown]` in union `Literal["local", "telegram", "discord", "whatsapp", "slack", ... omitted 17 literals] | set[Unknown]`
tests/plugins/web/test_parallel_keyless_mcp.py:19: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
plugins/web/parallel/provider.py:48: [unresolved-import] unresolved-import: Cannot resolve imported module `httpx`
tests/gateway/test_whatsapp_reply_prefix.py:119: [unsupported-operator] unsupported-operator: Operator `>=` is not supported between objects of type `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 31 union elements` and `int`
gateway/run.py:6425: [invalid-argument-type] invalid-argument-type: Argument to bound method `PairingStore._record_rate_limit` is incorrect: Expected `str`, found `Literal["local", "telegram", "discord", "whatsapp", "slack", ... omitted 17 literals] | set[Unknown]`
tests/agent/test_auxiliary_config_bridge.py:284: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `Literal["provider"]` and `Unknown | int | str | ... omitted 14 union elements`
hermes_cli/config.py:4708: [unresolved-attribute] unresolved-attribute: Attribute `items` is not defined on `str`, `list[Unknown]`, `list[str]`, `None`, `int` in union `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 31 union elements`
tests/cli/test_fast_command.py:484: [invalid-argument-type] invalid-argument-type: Argument to bound method `TestCase.assertIn` is incorrect: Expected `Iterable[Any] | Container[Any]`, found `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 31 union elements`
tests/agent/test_auxiliary_config_bridge.py:285: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `Literal["model"]` and `Unknown | int | str | ... omitted 14 union elements`
tests/hermes_cli/test_aux_config.py:47: [unsupported-operator] unsupported-operator: Operator `not in` is not supported between objects of type `Literal["session_search"]` and `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 31 union elements`
tools/file_operations.py:249: [invalid-assignment] invalid-assignment: Invalid subscript assignment with key of type `Literal["matches"]` and value of type `list[dict[str, str | int]]` on object of type `dict[str, int]`
... and 59 more

Unchanged: 5501 pre-existing issues carried over.

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

@elphamale
elphamale force-pushed the feat/telegram-group-media-routing branch from 6f21c53 to 7b0512b Compare June 17, 2026 13:24
@github-actions

Copy link
Copy Markdown

⚠️ MCP catalog security review required

This PR changes the bundled MCP catalog or MCP catalog installer code. MCP entries can define local commands that users later install into mcp_servers, so this needs explicit maintainer review before merge.

A maintainer should verify:

  • any new/changed optional-mcps/**/manifest.yaml command and args are expected,
  • stdio transports do not use shell+egress/exfiltration payloads,
  • git install refs are pinned and bootstrap commands are minimal,
  • requested env vars/secrets match the upstream MCP's documented needs.

After review, add the mcp-catalog-reviewed label and re-run this check.

@elphamale

Copy link
Copy Markdown
Owner Author

Closing — this was created against the wrong base. The actual fix is in NousResearch#47886.

@elphamale elphamale closed this Jun 17, 2026
elphamale pushed a commit that referenced this pull request Jul 20, 2026
…onnect ladder can't freeze silently (NousResearch#66377)

The Telegram gateway could go silently deaf for hours: the reconnect ladder
stalled mid-way (e.g. "attempt 4/10, reconnecting in 40s" then nothing) while
the process stayed active(running), so Restart=always never fired.

Root class: every recovery path — the ladder's re-entry
(_schedule_polling_recovery), the pending-update probe (_probe_pending_updates),
and PTB's error callback — gates new recovery on _polling_error_task.done(). If
that single task wedges on any hung await, all recovery returns early forever
and nothing retries.

The heartbeat loop is a separate task, so make it an independent, cause-agnostic
watchdog: if the same recovery task stays in-flight past
_POLLING_ERROR_TASK_STUCK_TIMEOUT (300s — well beyond a healthy ladder attempt's
bounded stop+drain+start+backoff), force a retryable-fatal so the background
reconnector rebuilds the adapter instead of relying on the frozen ladder. This
guarantees progress regardless of *where* the stall is (issue direction #1),
tracked locally so no task-assignment site needs to change.

Also salvages @koduri-mahesh-bhushan-chowdary's NousResearch#66492 (drain-await timeout),
which closes the one concrete wedge vector documented in the incident
(_drain_polling_connections' unbounded shutdown()/initialize() on a wedged
CLOSE-WAIT pool). The watchdog covers the rest of the class.

Co-authored-by: Koduri Mahesh Bhushan Chowdary <mkoduri73@gmail.com>
elphamale pushed a commit that referenced this pull request Jul 20, 2026
…reaming

Two real render-cost wins found by inspection (no behavior change):

1. Sidebar re-rendered on every stream token. $sessionStates is republished on
   every message delta (tens/sec during a turn), and the derived ID computeds
   ($workingSessionIds, $attentionSessionIds, $backgroundRunningSessionIds)
   allocated a fresh array each time. nanostores notifies on !==, so the whole
   ChatSidebar + every mounted row re-rendered per token even when the working/
   attention/background set was unchanged. Return the previous array reference
   when the contents match → nanostores skips the notify unless the set actually
   changes. Turns streaming from O(visible rows)/token into O(0) for the sidebar.

2. Tool rows normalized the FULL uncapped detail every render. `looksRedundant`
   (lowercase + whitespace-collapse over the entire read_file/terminal payload)
   ran twice in the ToolEntry render body, so every completed tool re-normalized
   its whole output on every stream tick of the running message. Memoize on the
   view fields so it recomputes only when the tool's content changes.

Both are correctness-preserving (stable refs + memoization). The CI stream
scenario drives $messages directly, not the publishSessionState path, so it
won't reflect #1 — verified by inspection.
elphamale pushed a commit that referenced this pull request Jul 25, 2026
…native extension)

unicode61 indexes a CJK run as ONE token, so 2-char Korean terms (일본,
구글, 우리, ...) can never match it and the trigram tokenizer needs >=3
chars per term — any query containing a 1-2 char CJK token falls through
to a LIKE full-table scan (measured 3-6.4s CPU per query on a 6.8GB
production state.db; the #1 base cost behind a 12.4s session_search
average on CJK workloads).

This ships a ~250-line loadable FTS5 tokenizer (no deps) that wraps
unicode61: maximal CJK runs inside its tokens are re-emitted as
overlapping character bigrams (Lucene CJKAnalyzer semantics), everything
else passes through unchanged. FTS5 phrase semantics turn consecutive
sub-tokens into exact substring matching down to 2-char terms at index
speed.

Build: native/fts5_cjk/build.sh -> ~/.hermes/lib/libfts5_cjk.so
(override: HERMES_FTS5_CJK_SO).

Salvaged from PR NousResearch#65544; the schema integration lands separately on the
v23 external-content layout.
elphamale pushed a commit that referenced this pull request Jul 25, 2026
…add same-pid self-reclaim guard

Hardening on top of the salvaged dead-PID lease reclamation from PR NousResearch#65775
(@the3asic):

- Probe via psutil.pid_exists (hard dependency; CONTRIBUTING.md critical
  rule #1) with the contributor's os.kill(pid, 0) POSIX probe retained
  only as a scaffold-phase fallback when psutil is missing.
- Same-process holders (pid == os.getpid()) are never probed and never
  self-reclaimed — another thread's live lease is owned by the lease
  refresher/release path.
- Any probe doubt (exceptions, permission errors) conservatively keeps
  the lease until normal TTL expiry; Windows stays TTL-only.
- Tests: psutil-first dead-pid reclaim (probe call pinned), os.kill
  fallback path, probe-doubt keeps lease, same-pid no self-reclaim,
  legacy holder + Windows paths assert NO probe via either API.
elphamale pushed a commit that referenced this pull request Jul 27, 2026
…ch#67140)

The background write guard decided ownership from `isinstance(usage_rec, dict)`,
so a local skill with NO usage record passed. That successful write called
bump_patch(), which created a `created_by: null` record — and the identical
write was refused from then on. "Allowed exactly once, then never" is a race
with our own bookkeeping, not a policy. Reproduced on main: patch #1 succeeds,
patch #2 with the same arguments is refused.

Option B from the issue. Option A (split `session_review` from
`scheduled_curator` and let the session fork patch user-owned skills it
consulted) would widen autonomous write permission onto skills the user owns
with no user present to consent — wrong direction for a no-user-present actor.

- skill_manager_tool: missing and explicit-null records now resolve
  IDENTICALLY, both fail closed. The refusal names the reason and points at
  `hermes curator adopt <name>`.
- background_review: both review prompts told the reviewer to patch any skill
  consulted in the session and claimed pinned skills could be improved, while
  enforcement refused both. Prompts now list pinned, external, and user-owned
  skills as protected, and tell the reviewer to RECOMMEND adoption instead of
  attempting a write that will be refused.
- skill_usage: document that `created_by` is a curator-management policy flag,
  not a provenance claim, and add `is_curator_managed()` so call sites read as
  the question they ask. Field name retained — it is on disk in every
  `.usage.json` and renaming would strand those records.
- curator CLI: `hermes curator list-unmanaged` itemizes unmanaged skills with
  the reason each is unmanaged (completes the NousResearch#67139 spec).

Foreground writes are untouched: a user-directed edit to a user-owned skill
still works, including on pinned skills.

Sibling tests: 9 failures in test_skill_manager_tool.py were fixtures that
created record-less skills to exercise OTHER guards (consolidation-delete,
read-before-write) and relied on ownership falling through. Fixed at the
fixture, since the real curator only ever operates on managed sediment. One
test asserted the old "manually authored" wording; rewritten to assert the
behavior contract instead of the string.

Validation: 274 targeted tests + all 7 background-review files (60 tests) pass.
E2E on a temp HERMES_HOME (30 checks) covers the flip, foreground writes,
adoption unblocking, pin semantics, prompt/enforcement parity, and the new verb.
Each new test sabotage-verified: revert the fix, confirm it goes red.

Fixes NousResearch#67140
elphamale pushed a commit that referenced this pull request Aug 2, 2026
…own (NousResearch#74136)

Fix-up for the cherry-picked cooldown persistence: the PR's tests mocked
the DB (SimpleNamespace(_db=MagicMock())), which cannot prove the cooldown
survives a restart. Replace with the production shape — a real SessionDB
on disk behind the real AsyncSessionDB facade — and add a restart
regression: fail a hygiene compression on runner #1, tear it down, build a
fresh GatewayRunner on the SAME database, and assert the cooldown is still
honored (no compression agent instantiated). Also updates the timeout test
to assert the DB-backed record_compression_failure_cooldown write instead
of the removed in-memory dict.

Sabotage-verified: reverting gateway/run.py to the in-memory dict makes
the restart test fail.
elphamale pushed a commit that referenced this pull request Aug 2, 2026
Users following abbreviated links guess /docs/quickstart and
/docs/installation and hit raw GitHub-Pages 404s — the real pages live
under /docs/getting-started/. Add client redirects for both.

Consumer-onboarding audit finding #1, Aug 2026.
elphamale pushed a commit that referenced this pull request Aug 3, 2026
The #1 patch failure class in production (state.db mining, 250k-window)
is a re-send of an edit that already landed: 'old_string and new_string
are identical' (299 occurrences) plus a share of hunk-not-found errors
where the new text is already in the file. These errored, sending
models into re-read/re-patch loops.

New tools/fuzzy_match.is_already_applied(content, old, new) — a
conservative check requiring (1) non-trivial new_string (>=8 chars),
(2) EXACT presence of new_string, (3) old_string gone (unless
identical). Wired into three sites:

- patch_replace (replace mode): returns success + no_change: true +
  an explicit note instead of the identical-strings / no-match error.
- V4A validation phase: an already-applied hunk validates as a no-op
  so multi-hunk patches no longer fail wholesale when one hunk landed
  in a prior call.
- V4A apply phase: mirrors the same skip so the two phases agree.

Genuine no-matches (new text absent) and half-applied renames (old
text still present) keep their error behavior — covered by tests.
elphamale pushed a commit that referenced this pull request Aug 3, 2026
process(action='wait') hitting its window returned status='timeout'
with a terse note — models read it as an error and re-issued identical
waits (process is the #1 exact-duplicate tool call in production: 511
dupes in a 400k-msg window; wait is 57% of all process actions).

The timeout result now carries:
- process_running: true — machine-readable 'this is a status, not a
  failure'
- an explicit note: 'Wait window of Ns elapsed — the process is still
  running. This is not an error. Uptime: Ms.' plus the right next step:
  when notify_on_complete is set, 'you will be notified on exit — do
  more work instead of waiting again'; otherwise a pointer to
  notify_on_complete for next time.
- the clamp note (requested > max) now composes with the status note
  instead of replacing it.

Exited/interrupted results are unchanged.
elphamale pushed a commit that referenced this pull request Aug 3, 2026
…e-review #1)

revoke_commit_admission() used to invoke the holder-qualified lease
release unconditionally — including while an admitted commit was still
mutating SessionDB — letting a second compressor acquire the durable
lock mid-commit and interleave with the first commit's writes.

The admission_revoked flag store stays lock-free, but the lease-release
decision now coordinates with the fence lock:
- revoke acquires the fence lock non-blocking; on success no commit can
  be in flight (an admitted commit retains the lock until finish_commit)
  and the release runs immediately, still under the lock so a racing
  begin_commit cannot slip between the check and the release.
- on failure the release is deferred: finish_commit() re-checks
  _admission_revoked and performs it AFTER the commit completes (prompt
  even if the worker thread is later parked), and the begin_commit
  refusal path does the same for a revoke that lost the race to a
  transient lock-setup/cancel boundary. All paths are idempotent with
  the worker's own outer cleanup (DB release is holder-qualified).

Invariant encoded + tested: no second compressor can acquire the durable
lock while an admitted commit is still mutating; after a post-revoke
commit finishes the lease is released promptly. Both regressions
(revoke-during-commit deferral, revoke-before-commit immediate release +
refused begin_commit) are sabotage-verified.
elphamale pushed a commit that referenced this pull request Aug 11, 2026
…rst run

The first-run provider picker showed Fireworks AI alongside Nous Portal
before the user opened the 'Other providers' disclosure. Only Nous Portal
should be visible up front; Fireworks now lives inside the expanded list
but keeps its #1 position there (Nous -> Fireworks ordering preserved).
elphamale pushed a commit that referenced this pull request Aug 15, 2026
Addresses both review findings on the remote-gateway download PR:

1. Unbounded buffering (finding #1). fetchBuffer / fetchBufferViaOauthSession
   accumulated the entire response (then copied it again via Buffer.concat)
   before saveGatewayFile even opened the save dialog, so a large gateway file
   could exhaust the native process. Both auth paths now stream: once response
   headers arrive the connect timeout is cleared, the filename is derived, the
   save dialog is shown, and the body is piped to the chosen destination with
   backpressure. A read/write error tears down the stream and unlinks the
   partial file. The byte-moving, data-URL decoding, and filename/path helpers
   are extracted into gateway-file-download.ts so they're unit-testable without
   Electron.

2. No fallback for older gateways (finding #2). saveGatewayFile required the new
   /api/fs/download route. Desktop and the remote gateway update independently,
   so a gateway predating this PR 404s. Added a 404-only compatibility fallback
   to the existing capped /api/fs/read-data-url route (bounded, so it only
   serves smaller files — enough to keep older backends working).

Tests: gateway-file-download.test.ts covers streaming, backpressure,
error-cleanup (unlink on write/response error), data-URL decoding, filename
derivation (incl. traversal reduction), and 404 detection;
gateway-file-download-transport.test.ts asserts both transports stream (no
whole-body Buffer.concat) and that the 404 fallback is wired. Both registered
in the desktop platform test list. Server-side /api/fs/download tests
(streaming + sensitive-file reject) already pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
elphamale pushed a commit that referenced this pull request Aug 15, 2026
…-renders (NousResearch#81726)

The scoped find walker wraps transcript text nodes in <mark> elements that
React does not own. Assistant responses stream through markdown-text.tsx,
which rebuilds the markdown DOM on every delta, and a new message is
appended whenever the assistant answers — so a re-render of a changed
region detaches the marks we inserted, dropping the user's highlights while
the bar stays open.

Watch the captured scope with a MutationObserver and re-wrap only when an
unmarked occurrence of the active query actually reappears. The observer is
gated behind a re-entrancy flag while the walker is mutating, coalesced to
one re-apply per microtask, torn down when the bar closes or the query
clears, and restores the active ordinal so a mid-stream re-render doesn't
reset the user's place to match #1. An append that adds no matching text is
a no-op; re-wrapping only fires when highlights genuinely went stale.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
elphamale pushed a commit that referenced this pull request Aug 18, 2026
Two independent bugs let a deleted profile reappear / leave orphaned
resources on next launch:

1. hermes_cli/profiles.py's backend-process scanner required argv[0] to
   resolve to an executable literally named "hermes". Electron's
   pool-backend spawn resolves the hermes console-script shim's path and
   execs it via the interpreter directly (python3 /path/to/hermes ...), so
   argv[0] reports as "python3" and the scanner never matched the running
   backend -- delete removed the profile's files but left its live backend
   process running (still bound to a port via uvicorn), which
   accumulates across repeated delete/recreate cycles.
2. The desktop sidebar's ProfileRail only refreshed its cached profile
   list once, on mount, so a delete/create/rename from another surface
   (another window, or the CLI) left a stale ghost entry until something
   unrelated triggered a refetch. Note: a delete via this window's own
   Manage-Profiles view already refreshes the shared $profiles atom
   ProfileRail subscribes to (confirmed by reading refreshProfiles() and
   handleConfirmDelete()) -- this fix only covers the cross-window/cross-
   process staleness gap, not a duplicate of the already-merged
   NousResearch#57329's Manage-Profiles rail-refresh work.

Fix 1: recognize a python-interpreter argv[0] exec'ing a hermes-named
console-script shim via argv[1]. Fix 2: refresh the profile list on window
focus/visibilitychange, matching the existing pattern used elsewhere in
the sidebar (sidebar/index.tsx, use-background-sync.ts, star-map.tsx,
use-gateway-boot.ts all use the same focus+visibilitychange pattern).

## Related work already on main

PR NousResearch#57329 (merged) fixed the *headline* symptom from issue NousResearch#52279
(deleted profile respawns) via a different, non-overlapping mechanism:
routing profile-delete through the primary backend instead of spawning a
fresh pool backend, plus a separate recreation guard in
ensure_hermes_home() (NousResearch#49435, merged) that makes a backend spawned into a
deleted profile's directory raise FileNotFoundError instead of silently
recreating it.

This PR is NOT a duplicate of that fix. Verified: even with both of those
merged, a backend process that survives because of gap #1 above still
holds a bound port via uvicorn -- it just can no longer resurrect the
profile directory. That's real resource-hygiene, not a symptom already
covered. Gap #2 touches a different file/component (ProfileRail /
profile-switcher.tsx) than NousResearch#57329's rail-refresh half (which touched the
Manage-Profiles view's own $profiles.ts / index.tsx) and covers a
distinct staleness path (cross-window/cross-process, not same-window
delete-then-refresh).

Tests: tests/hermes_cli/test_profiles.py -- 156 passed (existing +
regression coverage for the argv[0] python-interpreter detection case).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
elphamale pushed a commit that referenced this pull request Aug 20, 2026
posix.sh now probes `update --help` before the real update call; the fake
counted the probe as call #1, shifting the exits.N mapping so the retry
gate never fired. Answer the probe out-of-band so counted calls remain
actual update attempts.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant