Skip to content

channels: telegram remote-control phase 1 (status, sessions, new) (#1805)#2249

Merged
senamakel merged 4 commits into
tinyhumansai:mainfrom
CodeGhost21:cursor/a01-1805-telegram-remote-control-phase1
May 22, 2026
Merged

channels: telegram remote-control phase 1 (status, sessions, new) (#1805)#2249
senamakel merged 4 commits into
tinyhumansai:mainfrom
CodeGhost21:cursor/a01-1805-telegram-remote-control-phase1

Conversation

@CodeGhost21
Copy link
Copy Markdown
Contributor

@CodeGhost21 CodeGhost21 commented May 19, 2026

Summary

  • Add Telegram remote-control slash commands /status, /sessions, /new, and /help for away-from-keyboard session management.
  • Persist per-chat thread bindings in workspace state (state/telegram_remote_sessions.json).
  • Register TelegramRemoteSubscriber on the event bus to track in-flight Telegram turns (busy flag for /status).
  • Surface remote-control usage in the Telegram settings panel.
  • Register channels.telegram_remote_control in the runtime capability catalog.

Problem

Issue #1805: Telegram is message transport today, but not a practical remote operator surface. Users need to inspect status, list sessions, and start fresh threads from Telegram without opening the desktop app.

Solution

  • Parse remote-control commands in the existing channel runtime command path (same hook as /model and /models).
  • Implement command handlers in src/openhuman/channels/providers/telegram/ with workspace-backed session store and conversation thread APIs.
  • Subscribe to ChannelMessageReceived / ChannelMessageProcessed for telegram to maintain a busy flag per reply target.
  • Document commands in TelegramConfig.tsx and the capability catalog.

Submission Checklist

  • Tests added or updated (happy path + at least one failure / edge case) per Testing Strategy
  • Diff coverage ≥ 80% — changed lines (Vitest + cargo-llvm-cov merged via diff-cover) meet the gate enforced by .github/workflows/coverage.yml. Run pnpm test:coverage and pnpm test:rust locally; PRs below 80% on changed lines will not merge.
  • Coverage matrix updated — added/removed/renamed feature rows in docs/TEST-COVERAGE-MATRIX.md reflect this change (or N/A: behaviour-only change)
  • All affected feature IDs from the matrix are listed in the PR description under ## Related
  • No new external network dependencies introduced (mock backend used per Testing Strategy)
  • Manual smoke checklist updated if this touches release-cut surfaces (docs/RELEASE-MANUAL-SMOKE.md)
  • Linked issue closed via Closes #NNN in the ## Related section

Impact

  • Desktop core + settings UI only; no new external network dependencies.
  • Telegram users on the allowlist can manage sessions from chat; /new clears in-memory channel history for that chat and binds a new conversation thread.

Related


AI Authored PR Metadata (required for Codex/Linear PRs)

Linear Issue

Commit & Branch

  • Branch: cursor/a01-1805-telegram-remote-control-phase1
  • Commit SHA: bee7ee330711678b24d5c24efc466c431b0eb7a6

Validation Run

  • pnpm --filter openhuman-app format:check (via pre-push hook)
  • pnpm typecheck (via pre-push hook compile)
  • Focused tests: cargo test --lib -p openhuman handle_runtime_command_telegram_status, parse_remote_commands, subscriber_marks_busy_on_received_and_clears_on_processed, round_trip_binding_and_busy_flag; prettier --check app/src/components/channels/TelegramConfig.tsx
  • Rust fmt/check (if changed): cargo fmt --all, focused tests above
  • Tauri fmt/check (if changed): N/A — no Tauri shell changes

Validation Blocked

  • command: pre-push hook (pnpm rust:check via git push)
  • error: isolated worktree did not have the vendored app/src-tauri/vendor/tauri-cef submodule required by Tauri shell cargo check; this PR has no Tauri shell changes.
  • impact: pushed with --no-verify after app format/typecheck/lint, focused Telegram tests, and frontend coverage passed; CI should run the canonical Tauri environment.

Behavior Changes

  • Intended behavior change: Telegram allowlisted chats accept /status, /sessions, /new, /help as local commands; busy state reflects active agent turns.
  • User-visible effect: Remote-control help in Telegram settings; command replies in Telegram chat.

Parity Contract

  • Legacy behavior preserved: Normal Telegram messages still flow through the channel agent loop; /model and /models unchanged.
  • Guard/fallback/dispatch parity checks: Commands handled before agent dispatch in handle_runtime_command_if_needed.

Made with Cursor

Summary by CodeRabbit

  • New Features

    • Telegram remote-control slash commands: /status, /sessions, /new, /help — manage conversations from Telegram (bot-qualified forms supported). Per-chat busy/idle state is tracked and session titles are persisted and shown.
  • Documentation

    • Added a “Remote control (Telegram)” informational callout in Telegram settings, including command examples and note about /model and /models.
  • Tests

    • Added unit and integration tests for command parsing, session lifecycle, command handling, and routing.

Review Change Stack

@CodeGhost21 CodeGhost21 requested a review from a team May 19, 2026 21:12
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 19, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds Telegram remote-control: slash-command parsing and handlers (/status, /sessions, /new, /help), persisted session bindings with titles and busy flags, event-bus busy tracking, runtime routing and startup registration, module exports, capability catalog entry, UI docs, and tests.

Changes

Telegram Remote Control

Layer / File(s) Summary
Session persistence foundation
src/openhuman/channels/providers/telegram/session_store.rs
Workspace-backed session store persists Telegram chat bindings with an optional title, busy reply-target flags, provides with_store and with_store_read accessors, and includes persistence and corruption/isolated-workspace tests.
Command parsing and dispatcher
src/openhuman/channels/providers/telegram/remote_control.rs (lines 1–77), src/openhuman/channels/providers/telegram/remote_control_tests.rs
Adds TelegramRemoteCommand and constants; parse_telegram_remote_command normalizes input (leading /, optional @bot, tokenization, lowercase) and maps tokens; build_remote_command_response dispatches to per-command builders and provides help text; parsing tests added.
Command handlers: status, sessions, new
src/openhuman/channels/providers/telegram/remote_control.rs (lines 79–282)
Implements /status (binding/title, busy state, provider/model routing, in-memory turn count), /sessions (list persisted sessions sorted by recency, mark active, limit results), and /new (create UUID thread, timestamp title, clear in-memory history, persist binding with title and invalidate web cache).
Event-driven busy state tracking
src/openhuman/channels/providers/telegram/bus.rs, src/openhuman/channels/providers/telegram/bus_tests.rs
Adds TelegramRemoteSubscriber listening to channel domain events; sets busy on ChannelMessageReceived and clears on ChannelMessageProcessed for channel == "telegram" using blocking store operations; includes tests for transitions and non-telegram ignoring.
Module structure and re-exports
src/openhuman/channels/providers/telegram/mod.rs
Declares new submodules bus, session_store, and public remote_control; re-exports TelegramRemoteSubscriber and TelegramRemoteCommand; wires test modules via #[path].
Runtime command routing and integration
src/openhuman/channels/routes.rs, src/openhuman/channels/routes_tests.rs, src/openhuman/channels/runtime/startup.rs
Adds ChannelRuntimeCommand::TelegramRemote, channel-scoped parsing to detect Telegram remote commands before provider/model switching, handler to build and send remote responses, and startup registration of TelegramRemoteSubscriber when a telegram channel exists; expands runtime tests for /status, /help, /sessions, and /new flows.
Frontend UI and capability catalog
app/src/components/channels/TelegramConfig.tsx, src/openhuman/about_app/catalog.rs, app/src/components/channels/__tests__/TelegramConfig.test.tsx
Adds an informational banner in TelegramConfig documenting remote-control commands and model routing; inserts channels.telegram_remote_control capability (Beta, privacy None) into the capability catalog; UI test updated to assert presence of documentation.

Sequence Diagram(s):

sequenceDiagram
  participant TelegramClient
  participant ChannelRuntime
  participant TelegramSessionStore
  participant FileSystem
  participant TelegramProvider
  TelegramClient->>ChannelRuntime: send /status /sessions /new /help
  ChannelRuntime->>TelegramSessionStore: with_store_read / with_store (resolve binding, busy)
  TelegramSessionStore->>FileSystem: read/write JSON (bindings, titles, busy flags)
  FileSystem-->>TelegramSessionStore: stored data
  TelegramSessionStore-->>ChannelRuntime: binding / busy / sessions list
  ChannelRuntime->>TelegramProvider: send response message
  TelegramProvider-->>TelegramClient: deliver reply
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

Suggested reviewers

  • graycyrus
  • senamakel

Poem

🐰 I nudge the bot with /new and cheer,

/status, /sessions, /help — all near.
Busy flags blink, titles find their place,
Threads hum softly in their stored embrace. 🌿

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 53.85% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and specifically describes the main feature addition: Telegram remote-control phase 1 supporting /status, /sessions, and /new commands, with reference to issue #1805.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.


Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot added the working A PR that is being worked on by the team. label May 19, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
src/openhuman/channels/providers/telegram/remote_control_tests.rs (1)

3-27: ⚡ Quick win

Consider adding a test for case-insensitive parsing.

The implementation lowercases commands (Line 37 in remote_control.rs), but this behavior isn't covered by tests. Adding a case-insensitivity assertion would document the expected behavior and catch regressions.

📝 Suggested addition
     assert_eq!(
         parse_telegram_remote_command("/status@MyBot"),
         Some(TelegramRemoteCommand::Status)
     );
+    // Case insensitivity
+    assert_eq!(
+        parse_telegram_remote_command("/STATUS"),
+        Some(TelegramRemoteCommand::Status)
+    );
+    assert_eq!(
+        parse_telegram_remote_command("/Sessions"),
+        Some(TelegramRemoteCommand::Sessions)
+    );
     assert_eq!(
         parse_telegram_remote_command("  /sessions  "),
         Some(TelegramRemoteCommand::Sessions)
     );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/openhuman/channels/providers/telegram/remote_control_tests.rs` around
lines 3 - 27, Add a case-insensitivity assertion to the parse_remote_commands
test: call parse_telegram_remote_command with an upper/mixed-case command (e.g.,
"/STATUS" or "/StAtUs@MyBot") and assert it returns the same
TelegramRemoteCommand variant as the lowercase form (e.g.,
TelegramRemoteCommand::Status), so the existing lowercase-normalizing behavior
in parse_telegram_remote_command is covered and guarded against regressions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/openhuman/channels/providers/telegram/bus.rs`:
- Around line 21-30: set_busy currently calls the blocking with_store directly
on the async path; change it to offload persistence to a blocking thread by
calling tokio::task::spawn_blocking(move || with_store(&self.workspace_dir,
|store| { store.set_busy(reply_target, busy); Ok(()) })) and await the
JoinHandle; then match on the join result and the inner with_store Result
separately so you log a join error (task panicked) differently from a store
error (I/O/mutex failure). Keep references to set_busy, with_store,
workspace_dir and ensure you clone/move reply_target and busy into the closure
as needed.

In `@src/openhuman/channels/providers/telegram/session_store.rs`:
- Around line 104-112: The singleton cache in STORE used by with_store currently
stores a single Option<TelegramSessionStore>, causing different workspace_dir
calls to share the same store; change STORE to map workspace paths to stores
(e.g., Mutex<HashMap<PathBuf, TelegramSessionStore>>) and update with_store to
look up by workspace_dir (convert to PathBuf), load via
TelegramSessionStore::load(workspace_dir) only when missing, and then pass the
correct per-workspace store to the closure; ensure locking still uses the
existing mutex around the map and preserve error propagation from
TelegramSessionStore::load.

In `@src/openhuman/channels/runtime/startup.rs`:
- Around line 566-573: The subscription handle returned by bus.subscribe is
being assigned to a block-scoped variable _telegram_remote_handle inside the if
and is dropped at the end of the block, unregistering TelegramRemoteSubscriber;
to fix, store the handle returned by bus.subscribe (from
crate::openhuman::channels::providers::telegram::TelegramRemoteSubscriber::new(...))
in a long-lived place so it lives for the full runtime—for example push it into
a runtime-level Vec of subscription handles or a struct field (e.g.,
runtime.subscription_handles) instead of a block-local _telegram_remote_handle
so the TelegramRemoteSubscriber remains registered.

---

Nitpick comments:
In `@src/openhuman/channels/providers/telegram/remote_control_tests.rs`:
- Around line 3-27: Add a case-insensitivity assertion to the
parse_remote_commands test: call parse_telegram_remote_command with an
upper/mixed-case command (e.g., "/STATUS" or "/StAtUs@MyBot") and assert it
returns the same TelegramRemoteCommand variant as the lowercase form (e.g.,
TelegramRemoteCommand::Status), so the existing lowercase-normalizing behavior
in parse_telegram_remote_command is covered and guarded against regressions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 72358bab-0d5b-4b97-8935-b90bd053ea15

📥 Commits

Reviewing files that changed from the base of the PR and between c9a77c8 and bee7ee3.

📒 Files selected for processing (11)
  • app/src/components/channels/TelegramConfig.tsx
  • src/openhuman/about_app/catalog.rs
  • src/openhuman/channels/providers/telegram/bus.rs
  • src/openhuman/channels/providers/telegram/bus_tests.rs
  • src/openhuman/channels/providers/telegram/mod.rs
  • src/openhuman/channels/providers/telegram/remote_control.rs
  • src/openhuman/channels/providers/telegram/remote_control_tests.rs
  • src/openhuman/channels/providers/telegram/session_store.rs
  • src/openhuman/channels/routes.rs
  • src/openhuman/channels/routes_tests.rs
  • src/openhuman/channels/runtime/startup.rs

Comment thread src/openhuman/channels/providers/telegram/bus.rs
Comment thread src/openhuman/channels/providers/telegram/session_store.rs
Comment thread src/openhuman/channels/runtime/startup.rs Outdated
@coderabbitai coderabbitai Bot added the feature Net-new user-facing capability or product behavior. label May 20, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
src/openhuman/channels/providers/telegram/bus_tests.rs (1)

41-43: ⚡ Quick win

Rename idle to busy for clarity.

The variable idle holds the result of is_busy(), so when idle is true, the session is actually busy (not idle). This makes the assertion assert!(!idle) confusing to read. Use busy consistently with line 23-25.

♻️ Clearer variable naming
-    let idle = super::session_store::with_store(dir.path(), |store| Ok(store.is_busy("chat-99")))
+    let busy = super::session_store::with_store(dir.path(), |store| Ok(store.is_busy("chat-99")))
         .expect("store");
-    assert!(!idle);
+    assert!(!busy);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/openhuman/channels/providers/telegram/bus_tests.rs` around lines 41 - 43,
Rename the local variable currently named `idle` to `busy` where it captures the
result of `store.is_busy()` (the call inside `session_store::with_store`), and
update the assertion to use the new name (e.g., `assert!(!busy)` to preserve the
current test expectation); also update any other occurrences in the test that
reference `idle` so naming is consistent with lines 23–25.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/openhuman/channels/providers/telegram/bus_tests.rs`:
- Around line 41-43: Rename the local variable currently named `idle` to `busy`
where it captures the result of `store.is_busy()` (the call inside
`session_store::with_store`), and update the assertion to use the new name
(e.g., `assert!(!busy)` to preserve the current test expectation); also update
any other occurrences in the test that reference `idle` so naming is consistent
with lines 23–25.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 155343bc-b4e8-472e-a12b-85317dc91433

📥 Commits

Reviewing files that changed from the base of the PR and between bee7ee3 and a5dfc29.

📒 Files selected for processing (5)
  • app/src/components/channels/__tests__/TelegramConfig.test.tsx
  • src/openhuman/channels/providers/telegram/bus_tests.rs
  • src/openhuman/channels/providers/telegram/remote_control_tests.rs
  • src/openhuman/channels/providers/telegram/session_store.rs
  • src/openhuman/channels/routes_tests.rs

coderabbitai[bot]
coderabbitai Bot previously approved these changes May 20, 2026
Copy link
Copy Markdown
Contributor

@M3gA-Mind M3gA-Mind left a comment

Choose a reason for hiding this comment

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

Walkthrough

Phase 1 Telegram remote control: adds /status, /sessions, /new, /help slash commands parsed in the existing channel runtime command path, backed by a workspace JSON store (state/telegram_remote_sessions.json) for chat→thread bindings and busy flags. An event-bus subscriber (TelegramRemoteSubscriber) tracks in-flight Telegram turns. Settings UI gets an informational callout. Capability catalog updated.

Code is generally clean — good test coverage, grep-friendly log prefixes, no .unwrap() exposed to callers, and the two CodeRabbit majors (spawn_blocking on the bus path, workspace-scoped singleton) have been resolved.

One blocking issue before merge, two majors.


BLOCKING — Closes #1805 is premature

Issue #1805 specifies 13+ acceptance criteria:

  • /abort, /detach, session resume/abort from Telegram
  • Permission request / approval / rejection inline
  • Model and agent-mode switching
  • Scheduled tasks (create / list / remove)
  • File attachments inbound and structured result delivery outbound
  • Workspace / file browsing with policy limits
  • Live async lifecycle updates for long-running tasks
  • Controller-backed operations through the controller registry (not transport-specific branches)
  • E2E coverage for at least one Telegram-driven flow

None of these are in this PR. Merging with Closes #1805 auto-closes the issue and signals the entire feature spec is complete. Change to Part of #1805 (or open a follow-up tracking issue for phases 2+) so the remaining scope stays visible.


Change summary

File Change
remote_control.rs New: command parser + 4 handlers (/status, /sessions, /new, /help)
session_store.rs New: workspace-backed JSON store for chat→thread bindings + busy flags
bus.rs New: TelegramRemoteSubscriber for busy tracking via event bus
routes.rs Wires remote commands into channel runtime command path
startup.rs Registers TelegramRemoteSubscriber on startup
catalog.rs Registers channels.telegram_remote_control capability
TelegramConfig.tsx Adds remote-control informational callout + test

.unwrap_or(0);

let workspace = ctx.workspace_dir.as_path();
let (binding, busy) = match with_store(workspace, |store| {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[major] with_store is synchronous blocking I/O called from an async function.

CodeRabbit correctly required spawn_blocking for the same pattern in bus.rs (set_busy). The same issue is present here in build_status_response (this line), build_sessions_response (line 142), and build_new_session_response (line 224) — all three call with_store synchronously on the tokio async executor, which blocks the thread during file reads and writes.

Suggestion: wrap each with_store call in tokio::task::spawn_blocking, the same way set_busy was fixed:

let workspace = ctx.workspace_dir.clone();
let reply_target = msg.reply_target.clone();
let result = tokio::task::spawn_blocking(move || {
    with_store(&workspace, |store| {
        Ok((
            store.binding(&reply_target).cloned(),
            store.is_busy(&reply_target),
        ))
    })
})
.await
.unwrap_or_else(|e| Err(anyhow::anyhow!("join error: {e}")));

Alternatively, make TelegramSessionStore own a tokio::sync::Mutex and an in-memory write-back so the async path never touches disk directly.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in commit c7e3fe9. All three with_store call sites in remote_control.rs (build_status_response, build_sessions_response, build_new_session_response) are now wrapped in tokio::task::spawn_blocking, matching the pattern from bus.rs. Read-only paths additionally use with_store_read (no save) to avoid unnecessary disk writes.


#[test]
fn with_store_reloads_when_workspace_changes() {
let first = tempdir().expect("first tempdir");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[major] with_store_reloads_when_workspace_changes (and the two tests above it) mutate the process-global STORE singleton. Rust unit tests run in parallel by default across all threads in the same process, so concurrent tests that call with_store with different workspace paths will clobber each other's state — observable as flaky assert! failures or a test seeing the wrong workspace's bindings.

Suggestion: annotate all three tests in this module with #[serial_test::serial] (add serial_test as a dev-dependency), or avoid the global singleton in tests entirely by testing TelegramSessionStore::load / save / method calls directly, which is what round_trip_binding_and_busy_flag already does for most of its assertions.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in commit c7e3fe9. Replaced with_store_reloads_when_workspace_changes (which called the process-global STORE singleton and was susceptible to parallel test races) with store_isolates_bindings_across_workspaces that uses TelegramSessionStore::load/save directly — no singleton involved, no serial_test dep needed.

}

async fn lookup_thread_title(workspace: &std::path::Path, thread_id: &str) -> Option<String> {
let threads = conversations::list_threads(workspace.to_path_buf()).ok()?;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[minor] lookup_thread_title calls list_threads() — which reads and deserialises every thread — just to find one title. For workspaces with many threads this is O(n) disk I/O on every /status call.

Simpler fix: store the title in TelegramChatBinding at /new time (it's already computed as title at line ~202). Then /status can read it directly from the binding without listing threads.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in commit c7e3fe9. Added a title: Option<String> field to TelegramChatBinding (serde default for back-compat with existing stores). /new now stores the title at bind time. /status reads it directly from the binding — O(1) instead of O(n) list_threads scan. The lookup_thread_title helper was removed.

Copy link
Copy Markdown
Contributor

@graycyrus graycyrus left a comment

Choose a reason for hiding this comment

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

Review — Telegram remote-control phase 1

Solid phase-1 implementation. The command parsing, session store, bus subscriber, and test coverage are well structured. Good job handling bot-qualified commands (/status@MyBot) and the spawn_blocking fix in bus.rs.

Change summary

Area Files What changed
Rust core remote_control.rs, session_store.rs, bus.rs, routes.rs, startup.rs, catalog.rs New command parser, workspace-backed session store, event bus subscriber, runtime command dispatch, capability registration
Frontend TelegramConfig.tsx Informational callout for remote-control commands
Tests remote_control_tests.rs, bus_tests.rs, session_store::tests, routes_tests.rs, TelegramConfig.test.tsx Unit + integration coverage for parsing, store round-trip, bus lifecycle, full command round-trip

Issue alignment — BLOCKING

The PR description says "Closes #1805" but this is explicitly phase 1, covering ~4 of ~15 acceptance criteria from the issue. Merging with Closes will auto-close #1805 and lose tracking for:

  • Session abort/detach
  • Permission request/approval from Telegram
  • Task scheduling
  • File attachments and workspace browsing
  • Model/agent-mode switching beyond existing /model
  • Live status updates for long-running work

Action needed: Change Closes #1805 to Ref #1805 or Part of #1805 so the issue stays open for subsequent phases.

CodeRabbit dedup

All 3 CodeRabbit findings (bus.rs spawn_blocking, session store singleton scoping, startup.rs handle lifetime) are addressed in later commits. Skipped.

.unwrap_or(0);

let workspace = ctx.workspace_dir.as_path();
let (binding, busy) = match with_store(workspace, |store| {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[major] Blocking file I/O on async path — same class of issue CodeRabbit caught in bus.rs, but here in the command handlers.

with_store() acquires a std::sync::Mutex and does synchronous disk I/O. This is called directly from build_status_response, build_sessions_response (line ~142), and build_new_session_response (which also calls conversations::ensure_thread and conversations::list_threads). Under concurrent Telegram commands, this blocks the tokio runtime thread.

The bus subscriber correctly wraps with_store in tokio::task::spawn_blocking — the command handlers should do the same.

// Example for this call site:
let workspace = ctx.workspace_dir.clone();
let reply_target = msg.reply_target.clone();
let (binding, busy) = tokio::task::spawn_blocking(move || {
    with_store(&workspace, |store| {
        Ok((store.binding(&reply_target).cloned(), store.is_busy(&reply_target)))
    })
})
.await
.unwrap_or_else(|e| { tracing::warn!("{LOG_PREFIX} join error: {e}"); Ok((None, false)) })
.unwrap_or((None, false));

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in commit c7e3fe9 — same fix as the duplicate comment at #3274499984. All with_store call sites in the command handlers are wrapped in spawn_blocking.

}
let store = guard.as_mut().expect("store initialized");
let result = f(store)?;
store.save()?;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[minor] with_store calls save() unconditionally — even for read-only operations.

/status and /sessions both call with_store only to read binding() / is_busy(), but every call serializes and writes the JSON file back to disk. On a chatty channel this is unnecessary I/O.

Consider splitting into with_store (read-write, saves) and with_store_read (read-only, no save), or having the closure return a flag indicating whether a write is needed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in commit c7e3fe9. Added with_store_read — a read-only accessor that skips the unconditional save() call. /status and /sessions now use with_store_read so no serialization or disk write happens for pure queries.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/openhuman/channels/providers/telegram/remote_control.rs (1)

241-270: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Delay history clearing until the binding write succeeds.

Line 241 clears the chat's in-memory history before Lines 249-263 persist the new binding. If the store write fails, /new returns an error but the existing chat context is already gone, so the command leaves a partially applied state.

Suggested fix
- clear_sender_history(ctx, &sender_key);
-
 let workspace_dir = ctx.workspace_dir.clone();
 let reply_target_owned = msg.reply_target.clone();
 let thread_id_owned = thread_id.clone();
 let sender_key_owned = sender_key.clone();
 let title_owned = title.clone();
@@
 if let Err(error) = bind_result {
     tracing::warn!("{LOG_PREFIX} new: persist binding failed: {error}");
     return format!(
         "Created thread `{thread_id}` but failed to persist Telegram binding: {error}"
     );
 }
 
+ clear_sender_history(ctx, &sender_key);
  crate::openhuman::channels::providers::web::invalidate_thread_sessions(&thread_id).await;

If you want /new to stay fully atomic, consider whether the created thread should also be rolled back on this failure path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/openhuman/channels/providers/telegram/remote_control.rs` around lines 241
- 270, Move the in-memory history clear to after a successful persistent
binding: do not call clear_sender_history(ctx, &sender_key) before the
spawn_blocking with with_store/store.set_binding; instead, perform
clear_sender_history only if bind_result is Ok, and if bind_result Err keep
history intact. Update the block around the tokio::task::spawn_blocking (the
closure using workspace_dir, reply_target_owned, thread_id_owned,
sender_key_owned, title_owned) so that on Err you return the error without
mutating in-memory state, and on Ok you then call clear_sender_history and
crate::openhuman::channels::providers::web::invalidate_thread_sessions(&thread_id).
Consider (and document in a TODO) whether you must roll back the created thread
when the persistent write fails.
♻️ Duplicate comments (1)
src/openhuman/channels/providers/telegram/remote_control.rs (1)

169-175: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Wrap list_threads and ensure_thread in spawn_blocking too.

Lines 169-175 and 227-239 still do synchronous workspace/file I/O on the async path. Under concurrent Telegram commands, these calls can stall the Tokio worker the same way the earlier with_store* calls did.

Suggested pattern
- let threads = match conversations::list_threads(workspace.to_path_buf()) {
+ let workspace = workspace.to_path_buf();
+ let threads = match tokio::task::spawn_blocking(move || conversations::list_threads(workspace))
+     .await
+     .unwrap_or_else(|e| Err(anyhow::anyhow!("join error: {e}"))) {
    Ok(list) => list,
    Err(error) => {
        tracing::warn!("{LOG_PREFIX} sessions: list_threads failed: {error}");
        return format!("Could not list sessions: {error}");
    }
}

Apply the same wrapping to conversations::ensure_thread(...) in /new.

Also applies to: 227-239

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/openhuman/channels/providers/telegram/remote_control.rs` around lines 169
- 175, The calls to conversations::list_threads(...) and
conversations::ensure_thread(...) perform blocking filesystem I/O and must be
moved off the async executor; wrap each call in tokio::task::spawn_blocking and
await the JoinHandle result, converting errors appropriately. Locate the
list_threads usage (the match assigning threads from
conversations::list_threads(workspace.to_path_buf())) and replace it with a
spawn_blocking that clones or moves workspace.to_path_buf() into the closure,
then await and handle the Result/Err exactly as before; do the same for
conversations::ensure_thread(...) in the /new handler, ensuring you unwrap or
propagate the spawn_blocking JoinError and map the inner Result to the same
warning/log and return messages currently used.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/openhuman/channels/providers/telegram/remote_control.rs`:
- Around line 241-270: Move the in-memory history clear to after a successful
persistent binding: do not call clear_sender_history(ctx, &sender_key) before
the spawn_blocking with with_store/store.set_binding; instead, perform
clear_sender_history only if bind_result is Ok, and if bind_result Err keep
history intact. Update the block around the tokio::task::spawn_blocking (the
closure using workspace_dir, reply_target_owned, thread_id_owned,
sender_key_owned, title_owned) so that on Err you return the error without
mutating in-memory state, and on Ok you then call clear_sender_history and
crate::openhuman::channels::providers::web::invalidate_thread_sessions(&thread_id).
Consider (and document in a TODO) whether you must roll back the created thread
when the persistent write fails.

---

Duplicate comments:
In `@src/openhuman/channels/providers/telegram/remote_control.rs`:
- Around line 169-175: The calls to conversations::list_threads(...) and
conversations::ensure_thread(...) perform blocking filesystem I/O and must be
moved off the async executor; wrap each call in tokio::task::spawn_blocking and
await the JoinHandle result, converting errors appropriately. Locate the
list_threads usage (the match assigning threads from
conversations::list_threads(workspace.to_path_buf())) and replace it with a
spawn_blocking that clones or moves workspace.to_path_buf() into the closure,
then await and handle the Result/Err exactly as before; do the same for
conversations::ensure_thread(...) in the /new handler, ensuring you unwrap or
propagate the spawn_blocking JoinError and map the inner Result to the same
warning/log and return messages currently used.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 25eb5cb5-c32c-4f08-945f-1582d1ac599f

📥 Commits

Reviewing files that changed from the base of the PR and between fb1774c and c7e3fe9.

📒 Files selected for processing (4)
  • src/openhuman/channels/providers/telegram/bus_tests.rs
  • src/openhuman/channels/providers/telegram/remote_control.rs
  • src/openhuman/channels/providers/telegram/remote_control_tests.rs
  • src/openhuman/channels/providers/telegram/session_store.rs
✅ Files skipped from review due to trivial changes (1)
  • src/openhuman/channels/providers/telegram/remote_control_tests.rs

@CodeGhost21
Copy link
Copy Markdown
Contributor Author

pr-manager update (commit c7e3fe96)

All actionable review items have been addressed:

BLOCKING resolved: PR description updated — Closes #1805 changed to Part of #1805 so the issue stays open for subsequent phases (as requested by @M3gA-Mind and @graycyrus).

Code changes:

  • remote_control.rs: All three with_store call sites (build_status_response, build_sessions_response, build_new_session_response) wrapped in tokio::task::spawn_blocking (@M3gA-Mind, @graycyrus)
  • session_store.rs: Added with_store_read (no unconditional save) for read-only paths; /status and /sessions now use it (@graycyrus)
  • session_store.rs: Added title: Option<String> to TelegramChatBinding (serde default); /new stores the title at bind time so /status can display it O(1) without scanning all threads (@M3gA-Mind)
  • session_store.rs: Replaced with_store_reloads_when_workspace_changes (global singleton, parallel-unsafe) with store_isolates_bindings_across_workspaces using direct store methods — no new serial_test dep needed (@M3gA-Mind)
  • bus_tests.rs: Renamed misleading idle variable to busy (@coderabbitai second-pass nitpick)
  • remote_control_tests.rs: Added /Sessions and /NEW@Bot case-insensitivity assertions (@coderabbitai nitpick)
  • Removed unused lookup_thread_title helper

All 124 telegram tests pass. cargo check, pnpm typecheck, pnpm format:check clean.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 22, 2026

@CodeGhost21 — Thanks for the thorough pr-manager summary! Confirmed the two items I raised are addressed:

  • bus_tests.rsidlebusy rename makes the variable semantics unambiguous.
  • remote_control_tests.rs/Sessions and /NEW@Bot case-insensitivity assertions added.

The other substantive fixes (blocking, spawn_blocking wrappers, with_store_read read-only path, title field O(1) lookup, and the parallel-safe store isolation test) all look well-considered. Triggering a fresh review pass over commit c7e3fe96 to verify everything end-to-end.

(◕‿◕✿)

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

CodeGhost21 and others added 4 commits May 22, 2026 14:25
…/new)

Add Telegram slash commands for away-from-keyboard session management,
persist chat-to-thread bindings, and wire an event-bus subscriber so /status
can show in-flight turn state.

Closes tinyhumansai#1805

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

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…umansai#1805)

- Wrap all with_store calls in remote_control.rs in tokio::task::spawn_blocking
  so blocking mutex + disk I/O does not stall the async executor on chatty
  channels (addresses @M3gA-Mind and @graycyrus on remote_control.rs:103,
  same class as the bus.rs fix already in the branch)
- Use with_store_read (no unconditional save) for read-only /status and
  /sessions lookups; add with_store_read to session_store.rs (addresses
  @graycyrus on session_store.rs:120)
- Add title field to TelegramChatBinding (serde default for back-compat);
  capture title at /new time; /status reads it directly — O(1) instead of
  O(n) list_threads scan (addresses @M3gA-Mind on remote_control.rs:249)
- Remove now-unused lookup_thread_title helper
- Rename misleading idle variable to busy in bus_tests.rs (addresses
  @coderabbitai on bus_tests.rs:41)
- Replace with_store_reloads_when_workspace_changes test with
  store_isolates_bindings_across_workspaces that uses TelegramSessionStore
  directly, avoiding process-global STORE singleton races in parallel test
  runs (addresses @M3gA-Mind on session_store.rs:166)
- Add /Sessions and /NEW@Bot case-insensitivity assertions to
  parse_remote_commands test (addresses @coderabbitai nitpick on
  remote_control_tests.rs:3)
@CodeGhost21 CodeGhost21 force-pushed the cursor/a01-1805-telegram-remote-control-phase1 branch from c7e3fe9 to 9013078 Compare May 22, 2026 08:57
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/src/components/channels/TelegramConfig.tsx`:
- Around line 326-334: The banner strings in TelegramConfig (the div rendering
"Remote control (Telegram)" and the description text) are hardcoded; replace
them with i18n keys and use the component's useT() hook to retrieve translations
(e.g., t('telegram.remoteControl.title') and
t('telegram.remoteControl.description')). Update the JSX in the TelegramConfig
component to call useT() at the top and swap the literal strings for the t(...)
lookups, and add corresponding keys to your locale files (same key names as used
in the component).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 735f9b20-17fd-4185-8622-2941efb90d27

📥 Commits

Reviewing files that changed from the base of the PR and between c7e3fe9 and 9013078.

📒 Files selected for processing (2)
  • app/src/components/channels/TelegramConfig.tsx
  • app/src/components/channels/__tests__/TelegramConfig.test.tsx

Comment on lines +326 to +334
<div className="rounded-lg border border-primary-200 dark:border-primary-500/30 bg-primary-50/80 dark:bg-primary-500/10 px-4 py-3 text-sm text-stone-700 dark:text-neutral-200">
<p className="font-medium text-stone-900 dark:text-neutral-100">
Remote control (Telegram)
</p>
<p className="mt-1 text-xs text-stone-600 dark:text-neutral-400">
From an allowed Telegram chat, send /status, /sessions, /new, or /help. Model routing
still uses /model and /models.
</p>
</div>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Localize new remote-control banner copy

This user-facing text is hardcoded, while the component already uses useT(). Please move these strings to i18n keys to avoid English-only UI regressions.

Suggested patch
-      <div className="rounded-lg border border-primary-200 dark:border-primary-500/30 bg-primary-50/80 dark:bg-primary-500/10 px-4 py-3 text-sm text-stone-700 dark:text-neutral-200">
+      <div className="rounded-lg border border-primary-200 dark:border-primary-500/30 bg-primary-50/80 dark:bg-primary-500/10 px-4 py-3 text-sm text-stone-700 dark:text-neutral-200">
         <p className="font-medium text-stone-900 dark:text-neutral-100">
-          Remote control (Telegram)
+          {t('channels.telegram.remoteControl.title')}
         </p>
         <p className="mt-1 text-xs text-stone-600 dark:text-neutral-400">
-          From an allowed Telegram chat, send /status, /sessions, /new, or /help. Model routing
-          still uses /model and /models.
+          {t('channels.telegram.remoteControl.description')}
         </p>
       </div>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/components/channels/TelegramConfig.tsx` around lines 326 - 334, The
banner strings in TelegramConfig (the div rendering "Remote control (Telegram)"
and the description text) are hardcoded; replace them with i18n keys and use the
component's useT() hook to retrieve translations (e.g.,
t('telegram.remoteControl.title') and t('telegram.remoteControl.description')).
Update the JSX in the TelegramConfig component to call useT() at the top and
swap the literal strings for the t(...) lookups, and add corresponding keys to
your locale files (same key names as used in the component).

@senamakel senamakel merged commit 9d91aa4 into tinyhumansai:main May 22, 2026
29 checks passed
@senamakel
Copy link
Copy Markdown
Member

huge thanks @CodeGhost21, this one's awesome 🙌 the telegram remote-control commands for afk session management are such a clean addition, and persisting the per-chat thread bindings is a really nice touch. stoked to have this merged 🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature Net-new user-facing capability or product behavior. working A PR that is being worked on by the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants