Skip to content

feat: multi-session router with Telegram forum topic support#2

Merged
maciek-hyperdev merged 20 commits into
gohyperdev:mainfrom
Filyus:feature/multi-session-router-telegram-topics
May 24, 2026
Merged

feat: multi-session router with Telegram forum topic support#2
maciek-hyperdev merged 20 commits into
gohyperdev:mainfrom
Filyus:feature/multi-session-router-telegram-topics

Conversation

@Filyus

@Filyus Filyus commented Apr 12, 2026

Copy link
Copy Markdown
Contributor

Features

Why:

  • Telegram allows only one polling connection per bot token — running two Claude Code sessions with the same TELEGRAM_BOT_TOKEN yields 409 Conflict on getUpdates.
  • The usual workaround (a separate bot per session) fragments chats across bots, requires a new /newbot and token per project, and leaves you guessing which bot's DM belongs to which session.

What the router does:

  • One bot, many sessions — hdcd-router holds the single polling connection and multiplexes sessions across forum topics of a shared supergroup.
  • Each session gets a fresh topic on first connection; history never bleeds between sessions.
  • New MCP tool set_topic_title — Claude renames its own topic once the conversation theme becomes clear (the initial name is the project folder plus a short session ID).
  • Sessions start automatically on first connection and the router shuts itself down when idle.

Claude Code does not expose a stable session identity (#20132, #25642, #38390) — the router assigns its own UUID per process and uses it to correlate messages with topics.

Usage

Router mode is opt-in via the --router flag:

{
  "mcpServers": {
    "telegram": {
      "command": "hdcd-telegram",
      "args": ["--router"]
    }
  }
}

Config (~/.claude/channels/telegram-router/config.json):

{
  "bot_token": "...",
  "supergroup_id": "-100xxxxxxxxxx",
  "allowed_users": ["<your-telegram-user-id>"]
}

The supergroup must have Topics enabled (Telegram → Group Settings → Topics), and the bot must be an admin with Manage Topics permission. To find the supergroup_id: send a message in the group, then call https://api.telegram.org/bot<TOKEN>/getUpdates — the ID is .result[].message.chat.id (negative number starting with -100). The same response has .from.id for allowed_users.

Summary

  • Adds hdcd-router — a long-running process that manages concurrent sessions and coordinates with the Telegram Bot API.
  • Topics are created, closed, and never reused across sessions.
  • Outbox uses ACK-based position tracking: messages aren't marked delivered until the Telegram send succeeds (crash-safe).

Changes

  • New module src/router/ (config, mailbox, sessions, topics, main).
  • Extended src/main.rs with a router subcommand.
  • Telegram API: added forum topic create/close/reopen/editForumTopic calls.
  • New MCP tool set_topic_title (router mode only).
  • CI/release workflows updated for the new binary.
  • Integration tests: tests/router_ipc_test.rs.

@Filyus
Filyus force-pushed the feature/multi-session-router-telegram-topics branch 8 times, most recently from d3f3ef2 to 3145e63 Compare April 12, 2026 13:53
@maciek-hyperdev

Copy link
Copy Markdown
Contributor

Thanks for this — genuinely impressed with the quality of the work. Clean module boundaries, OS-level file lock for the mutex (instead of PID heartbeat), peek+commit-after-send for at-least-once outbox semantics, reconcile-on-startup for orphans, graceful VS Code refusal, idle auto-shutdown, zero unsafe outside the two documented PID-liveness checks. The comments explain why (not what), the tests cover the non-obvious invariants (peek_skips_malformed_via_next_offset, commit_per_message_enables_at_least_once), and the architecture is coherent end-to-end.

Did a thorough review. One blocker and a few should-fixes before we can merge / release.

Must-fix (blocker)

1. libc not declared as a dependency. src/router/sessions.rs:154 uses libc::kill(pid as i32, 0), but libc isn't in Cargo.toml — only transitive. cargo check --all-targets fails on macOS/Linux with E0433: failed to resolve: use of unresolved module libc. The Windows branch compiles (windows-sys is declared), so CI might not have caught this if it only builds Windows first. Fix:

[target.'cfg(unix)'.dependencies]
libc = \"0.2\"

Could you also add cargo test --all-targets to ci.yml on both Linux and macOS runners? Right now CI builds but doesn't run tests for the router modules.

Should-fix (before v0.2.0 release)

2. Fail-fast on missing hdcd-router / config.json in --router mode. Currently ensure_router_running() returns Ok(()) with only a warn! when either is missing. The MCP worker keeps running in router mode, writes to register/<sid>.json, polls inbox — but nothing ever arrives, because there's no router. From Claude Code's perspective the MCP server is "connected" and the user has no idea why the channel is silent. Proposal: bail! with an actionable message ("install hdcd-router next to hdcd-telegram and create config.json at ") so the startup fails loudly.

3. supergroup_id should be parsed as i64 in config::load. Currently stored as String and compared via chat_id.to_string() != s.config.supergroup_id. If the user types the ID with a leading space, a typo in the -100 prefix, or anything else slightly off, the router silently drops every incoming message with no clear signal. Parse once, validate once, fail with a clear error message.

4. File permissions (Unix). The router state directory currently relies on the user's umask. For defense in depth against other local processes on the same machine, chmod the state dir 0700 and the files (config.json, sessions.json, register/*, inbox/*.jsonl, outbox/*.jsonl) to 0600 on creation. This matches the existing behavior for ~/.claude/channels/telegram/.env in standalone mode. Worth also warning at startup if config.json is world-readable (same pattern as standalone's .env warning).

5. allowed_users check only covers update.message. handle_update filters messages through is_allowed, but other update types (callback_query, edit_message, message_reaction, channel_post) are silently ignored rather than allowlist-gated. For the current feature set this is fine (no callback queries in router mode yet), but if inline keyboards are added later (e.g. permission relay), the allowlist will be bypassed. An explicit else { return Ok(()) } with a comment would prevent future footguns.

6. HDCD_TELEGRAM_BOT_TOKEN / ~/.claude/channels/telegram/.env fallback. Right now router mode requires a fresh config.json — existing v0.1.2 users can't reuse their token. Suggestion: if config.json doesn't contain a bot_token field (or is missing), fall back to reading the env/.env file like standalone mode does. Reduces migration friction.

Nice-to-have (follow-up PRs are fine)

  • Test for reconcile-stale-sessions-on-startup (dead PID in sessions.json → topic gets closed). Currently covered only by the health check path.
  • Test for lock contention — second hdcd-router process on the same state dir should cleanly refuse to start. The fs4::try_lock_exclusive logic is correct but untested.
  • Test for ensure_router_running auto-spawn (currently the integration test skips it because there's no config.json in the temp dir).
  • Consider moving to clap if more flags are planned — the ad-hoc args.iter().any(|a| a == \"--router\") pattern doesn't scale, though it's fine for now.

On compatibility with v0.1.2

Confirmed the opt-in design is clean — users without --router see no change in behavior. Standalone mode's load_token(), .env, HDCD_TELEGRAM_BOT_TOKEN are untouched. Separate state directory (telegram-router/) means no risk of corrupting existing installs.

One thing we'll need to handle on our end (not in this PR): the plugin launcher in gohyperdev/hdcd-telegram-plugin needs to copy both binaries to the plugin data directory, and our /telegram:configure / /telegram:access skills need to understand router-mode paths. That's release prep for v0.2.0, not a blocker for merging.


Again, really solid work. Happy to merge as soon as the blocker is fixed — should be a one-liner on your end. The should-fixes can land in this PR or as a follow-up, your call. If you'd prefer we handle (3)–(6) in a separate PR after merging, that's fine too.

@maciek-hyperdev

Copy link
Copy Markdown
Contributor

Hi @Filyus 👋 — wanted to check in. Any blockers on the libc fix or the other review items? Happy to help out if you're stuck, or to split the should-fixes into a follow-up PR after merging the current state (once the compile blocker is resolved). This is solid work and I'd hate to see it stall.

For what it's worth, we just merged #4 from @AsgardMuninn — so the repo is actively moving. Your router architecture is the right next step for v0.2.0.

Filyus added 13 commits April 20, 2026 09:29
Add claude_session module that discovers Claude's PID via parent-chain
walk and reads the stable sessionId from ~/.claude/sessions/{PID}.json.
A background watcher in the MCP detects sessionId changes at runtime
(interactive --resume picker, /resume, /clear) and rewrites the
registration file so the router rebinds forum topics on next poll.

Key changes:
- Session registry stores claude_session_id and message_count per entry
- register() renamed to bind(); evicts prior topic owner on reattach,
  carries message_count so reattached topics aren't treated as parasitic
- reconcile_session() replaces ensure_topic(): handles no-op, first
  registration, and mid-session rebind in one path
- Parasitic topics (zero activity) are deleted instead of closed to keep
  the supergroup clean; topics with history are only closed
- Router stderr redirected to state_dir/router.log to prevent broken-pipe
  kills when the spawning MCP exits before the router
- HDCD_SKIP_SESSION_DISCOVERY env var disables discovery in tests
Moves `load_token` out of main.rs into a new `src/token.rs` so router
mode can reuse the same env / `.env` fallback logic. Adds
`try_load_token` (returning `Option`) alongside the hard-fail
`load_token`, plus unit tests covering priority order, quote
stripping, and the "nothing configured" branch.

No behavior change for standalone mode.
Introduces `secure_dir` (0700) and `secure_file` (0600) as Unix-only
chmod helpers, with Windows no-op stubs. Call-site wiring for the
router state directory and its files follows in a subsequent commit.
`RouterConfig.supergroup_id` is now `i64`. A custom deserializer
accepts either a JSON number (`-1001234567890`) or a JSON string
(`"-1001234567890"`, whitespace tolerated) so users can paste the id
in whichever form their tools emit. Invalid strings, overflowing
numbers, and zero are rejected up-front with a clear error — a typo
in the `-100` prefix no longer silently drops every inbound message.

Call-sites that need the string form for Bot API use the new
`RouterConfig::chat_id_str()` helper. The supergroup filter in
`handle_update` is now a direct `i64 == i64` comparison.
`bot_token` in `config.json` is now optional: if it's missing or
empty, the router resolves it via the standalone channel's discovery
chain — `HDCD_TELEGRAM_BOT_TOKEN`, `TELEGRAM_BOT_TOKEN`, then
`~/.claude/channels/telegram/.env`. This lets existing v0.1.2 users
flip on router mode without duplicating their token into a fresh
config file.

If nothing resolves, the error now lists all four discovery paths
instead of just the missing config field.

Adds `token::with_isolated_token_env` as a shared test harness so
tests in `token` and `router::config` can safely manipulate the
process-global env vars under one mutex.
`ensure_router_running` previously warned and returned `Ok` if
`config.json` or the `hdcd-router` binary was missing. The MCP
server kept running in router mode with no peer, silently accepting
tool calls and polling an inbox that nothing ever wrote to — from
Claude's perspective the channel was connected, but nothing arrived.

Both missing-prerequisite paths now `bail!` with actionable error
messages pointing to the expected location and suggesting the
standalone fallback.

Adds an `HDCD_SKIP_ROUTER_LAUNCH` env escape hatch so the IPC
integration test can exercise file plumbing without a live router
peer.
Every file and directory the router creates under its state dir is
now chmod'd 0600 (files) or 0700 (dirs) on Unix:

- router state dir itself (`config::state_dir`)
- `inbox/`, `outbox/`, `register/` subdirs (`mailbox::ensure_dirs`)
- per-session registration JSON written by the MCP binary
- `sessions.json` persisted by `SessionRegistry::save`
- `router.lock` heartbeat and `router.guard` OS lock
- inbox/outbox JSONL files and `.pos` companions

Same defense-in-depth posture the standalone channel already had for
`.env`. Windows paths are no-ops (NTFS ACLs inherit user-private
from the profile directory).
`config::load` now calls `fs_perms::warn_if_world_readable` on
`config.json` before reading it. Mirrors the existing `.env` check
in standalone mode — a misconfigured perm bit gets logged at startup
with a `chmod 600` hint instead of silently leaking the bot token to
other local users.

Consolidates the shared warning logic in `fs_perms` so both token
discovery and router config use the same path.
Adds a comment documenting that any future update variant must be
gated by is_allowed() before session-affecting work. The early-return
now logs which variant was ignored (callback_query vs other) to make
silent drops visible in debug traces.

Purely documentation/observability — no behavior change.
The check-job already runs tests on Ubuntu, but the Build-job only
compiles. Adds a dedicated Test-job with a matrix covering ubuntu-latest
and macos-latest to catch unix-specific regressions (file perms, tempdir
behavior) before they land on main.
Extracts PID-liveness filtering from close_dead_sessions into
SessionRegistry::dead_session_ids so the decision is testable without
a TopicManager. Adds three tests:

- dead_session_ids_finds_sessions_with_nonexistent_pid: u32::MAX PID
  is treated as dead; None-pid sessions are not
- dead_session_ids_ignores_closed_sessions: only active sessions
  contribute to the reconcile list
- lock_guard_is_exclusive: a second acquire fails fast (non-blocking)
  and re-acquire succeeds after the first guard drops
Applies cargo fmt across router sources touched during the v0.2.0
work, and converts the is_some/unwrap pattern in reconcile_session
to an if-let so clippy passes under -D warnings. No behavior change.
@Filyus
Filyus force-pushed the feature/multi-session-router-telegram-topics branch from 3145e63 to c0a1538 Compare April 20, 2026 05:23
@Filyus

Filyus commented Apr 20, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the thorough review, really appreciated the detail. Pushed a series of commits on feature/multi-session-router-telegram-topics that close the blocker plus every should-fix. Summary:

Must-fix

  1. libc was already declared as a cfg(unix) dep in main. Added a Test job to .github/workflows/ci.yml with an ubuntu-latest + macos-latest matrix running cargo test --all-targets --all-features, so the router modules are now exercised on both.

Should-fix

  1. ensure_router_running now bail!s with an actionable message when either config.json or the hdcd-router binary is missing. Also threaded an HDCD_SKIP_ROUTER_LAUNCH escape hatch so the integration test can drive IPC without spawning a real subprocess.
  2. supergroup_id is now i64, parsed via a custom de_chat_id visitor that accepts both JSON integer and string forms, trims whitespace, and rejects garbage with a message that includes the expected format. RouterConfig::chat_id_str() returns the string form for API calls. All downstream comparisons (handle_update, etc.) are now direct i64 equality.
  3. Added src/fs_perms.rs with secure_dir (0700), secure_file (0600), and warn_if_world_readable helpers (no-ops on Windows). Applied on the state dir, config.json, sessions.json, register/*.json, inbox/*.jsonl, outbox/*.jsonl, router.lock, and router.guard. config::load now emits the same world-readable warning the standalone .env loader does.
  4. handle_update's early-return for non-message updates now logs which variant was ignored (callback_query vs other) at debug level, and the function carries an ALLOWLIST INVARIANT comment telling future contributors that any new variant must be gated through is_allowed() before session-affecting work.
  5. Extracted the token-discovery logic into src/token.rs, shared between standalone and router. RouterConfig::bot_token is now #[serde(default)]: if the field is missing or empty, config::load falls back through HDCD_TELEGRAM_BOT_TOKEN, then TELEGRAM_BOT_TOKEN, then ~/.claude/channels/telegram/.env. The error message on total absence points at all four sources.

Nice-to-have tests

  • Reconcile: extracted the dead-PID decision into SessionRegistry::dead_session_ids() so it's testable without a TopicManager. Two unit tests: a session bound to u32::MAX is surfaced, and closed sessions are not.
  • Lock contention: unit test acquires a guard, confirms the second try_lock_exclusive fails fast (does not block), and that a fresh acquire succeeds once the first guard drops.
  • ensure_router_running auto-spawn: left for a follow-up since it needs a process mock.

Extra (beyond the review)

One bigger feature landed on top of the router work while this was in flight: forum topics now stay attached to the same logical conversation across claude --resume, /resume, the interactive resume picker, and /clear. Before this, every resume started a fresh MCP subprocess, the router saw it as a brand-new session, created a brand-new topic, and left the original orphaned in the supergroup.

The fix is to track Claude Code's stable sessionId (not just our own ephemeral router session_id). The MCP discovers it on startup by walking its parent-process chain up to Claude and reading ~/.claude/sessions/{PID}.json, then watches that file for changes during the session. When the sessionId changes, the MCP updates its registration and the router rebinds the existing topic to the new session instead of creating a new one. Topics that never saw any real traffic get deleted on rebind (so the supergroup stays clean), while topics with real history are kept and only closed.

There was also a small infra fix that fell out of this: the router's stderr is now redirected to a log file in the state dir rather than inherited from the spawning MCP, so the router doesn't get killed by SIGPIPE if the MCP exits first.

Deferred

  • clap migration. Per your note, leaving this for a follow-up.
  • ensure_router_running auto-spawn integration test.

All green locally: cargo fmt --check, cargo clippy --all-targets -- -D warnings, cargo test --all-targets (51 tests). Branch is synced to main and ready for another look when you have a moment.

@maciek-hyperdev

Copy link
Copy Markdown
Contributor

Outstanding turnaround — every review item landed, plus the session-tracking feature (d39ff65) is a really nice piece of work. Orphan topics on --resume / /resume / /clear have been a real pain point, and the parent-walk + ~/.claude/sessions/{pid}.json approach is the right call given Claude Code doesn't expose a stable session ID to MCP servers.

Re-reviewed everything. Build is clean (cargo check, cargo clippy -- -D warnings), all 45 of 46 tests pass, no new unsafe beyond the minimal syscall wrappers, no new dependencies of concern. Security audit on the session-tracking feature came up clean: parent walk is bounded (MAX_PARENT_WALK = 8), paths are not user-controlled, serde_json recursion is default-capped, topic rebinding evicts previous owner atomically.

One blocker before merge:

is_pid_alive(u32::MAX) returns true on Unix (src/router/sessions.rs:248). This is simultaneously a failing test and a subtle production bug:

unsafe { libc::kill(pid as i32, 0) == 0 }

u32::MAX as i32 becomes -1, and POSIX kill(-1, 0) has special meaning — "send to every process I can signal" — so it returns 0 (success) whenever any process the user can signal is alive. The failing test (dead_session_ids_finds_sessions_with_nonexistent_pid) reproduces deterministically on macOS because there are always user-owned processes around. The production consequence: a session registered with Some(u32::MAX) would never be reaped.

One-liner fix at the top of is_pid_alive:

if pid == 0 || pid > i32::MAX as u32 {
    return false;
}

That plugs both the test and the underlying bug.

Follow-up items (not blocking):

  • macOS parent walkingparent_of() reads /proc/<pid>/stat, which doesn't exist on macOS, so on macOS the walk stops after depth 1 (getppid() only). For the common zsh → claude → hdcd-telegram shape that's enough, but wrappers (VS Code devcontainer, tmux, asdf/direnv) add layers and the sessionId discovery falls back to "no rebind". Either gate the stat path with #[cfg(target_os = "linux")] and return None explicitly on macOS for now, or reach for libc::sysctl with KERN_PROC_PID. Happy to take this as a follow-up PR after merge if you'd rather not context-switch.
  • secure_file after watcher-rewrite registration (src/main.rs:523) — defense in depth, the inode doesn't change on std::fs::write so the 0600 is preserved in practice, but an explicit call would match the pattern used elsewhere.
  • Warn when supergroup_id > 0 — Telegram supergroups are always -100...; a positive ID will fail every API call downstream, so a warn! in config::load would save someone a confused debugging session.

One CI note: the matrix job you added is exactly what we needed (ubuntu + macos running cargo test --all-targets --all-features), but first-time contributor workflows require maintainer approval to run. Right now the 5 workflow runs are in action_required. I'll approve them so CI actually executes — that'll validate the macOS build across a clean environment too.

Once M1 is fixed and CI goes green, I'll merge. Really solid contribution.

Filyus added 4 commits April 25, 2026 09:59
Re-apply secure file permissions after the Claude session watcher rewrites its registration file.

std::fs::write normally preserves permissions for the existing inode, but this keeps watcher rewrites consistent with the initial registration write and hardens edge cases where the file was recreated or permissions changed unexpectedly.
Emit an early warning when config.json contains a positive supergroup_id.

Telegram supergroup chat IDs normally use the -100... form, so accepting a positive value would likely lead to confusing downstream API failures.
@Filyus

Filyus commented Apr 25, 2026

Copy link
Copy Markdown
Contributor Author

Thanks again for the careful review and the kind words.

Fixed the blocker and the three follow-up items:

  • is_pid_alive now rejects pid == 0 and values above i32::MAX before calling kill, so u32::MAX no longer turns into kill(-1, 0).
  • parent_of is now Linux-gated for /proc, with macOS implemented via sysctl(KERN_PROC_PID) so parent walking works through wrapper layers there too.
  • The sessionId watcher re-applies secure_file after rewriting registration.
  • config::load now warns when supergroup_id is positive.

CI is currently waiting on maintainer approval (action_required), so once that runs we should get the clean macOS validation too.

@Filyus

Filyus commented Apr 25, 2026

Copy link
Copy Markdown
Contributor Author

Quick follow-up: I spoke too soon on the macOS fix.

The initial follow-up used sysctl(KERN_PROC_PID) with libc::kinfo_proc, but macOS CI showed that kinfo_proc is not available from the libc crate for the Apple targets we build. So the intent matched one of the suggested approaches, but the implementation still wasn't portable.

I pushed a new fix: macOS parent walking now uses a target-specific libproc dependency instead of direct libc::kinfo_proc/sysctl. Linux still uses /proc/<pid>/stat, and Windows is unchanged.

This should remove the macOS CI blocker.

@maciek-hyperdev

Copy link
Copy Markdown
Contributor

Excellent turnaround on every review item. M1 (is_pid_alive guard) is exactly the one-liner we needed; secure_file re-apply lands in both write paths (src/main.rs:460 and :530); the supergroup_id > 0 warning has a clear, actionable message. README expansion (+128/-1) covers router-mode setup, forum-topics-per-session, and the --router flag — solid.

I re-approved CI after your latest push. Linux + Windows + clippy/fmt are clean, but all three macOS jobs fail:

  • Build (aarch64-apple-darwin) — failure
  • Build (x86_64-apple-darwin) — failure
  • Test (macos-latest) — failure

Reproduced locally on aarch64-darwin (cargo 1.95.0, libc 0.2.184):

error[E0425]: cannot find type `kinfo_proc` in crate `libc`
  --> src/claude_session.rs:96
   |
96 |     let mut proc_info: libc::kinfo_proc = unsafe { std::mem::zeroed() };

libc exposes kinfo_proc only for freebsd/openbsd/netbsd/dragonfly, not macOS — even though the C header on Apple platforms defines the type. There's a long-standing open issue against rust-lang/libc about this; the upstream fix isn't there yet.

Blocker — one fix, three paths

1 (recommended) — Command::new("ps"). Trade slightly higher cost (~5–10 ms per call, run a handful of times per session lifetime) for zero unsafe, zero new deps, and parity with the Linux path (which also uses an OS facility — /proc/{pid}/stat):

#[cfg(target_os = \"macos\")]
fn parent_of(pid: u32) -> Option<u32> {
    if pid == 0 || pid > i32::MAX as u32 { return None; }
    let out = std::process::Command::new(\"ps\")
        .args([\"-o\", \"ppid=\", \"-p\", &pid.to_string()])
        .output().ok()?;
    if !out.status.success() { return None; }
    std::str::from_utf8(&out.stdout).ok()?.trim().parse().ok()
}

Lands the PR fast and keeps the macOS code in the same readability tier as Linux. We can revisit if the cost ever bites in practice (it won't — this is on the cold path, called only when the watcher resolves a Claude PID).

2 — libproc crate (pidinfo::<BSDInfo>(pid as i32, 0)?.ppid as u32). Clean strongly-typed API, BSD-licensed, ~50 KB to the binary. Reasonable if we expect more macOS process introspection later.

3 — Manual #[repr(C)] struct kinfo_proc { … }. Most fragile: Apple's eproc layout differs by SDK and arch (kp_proc 296 + kp_eproc; e_ppid offset is non-trivial). I'd avoid unless we ship a unit test pinning the layout against sysctl(KERN_PROC_PID) on every CI macOS image.

My vote: (1).

Smaller items (do whatever's quickest)

  • len >= mem::size_of::<…>() check after sysctl — only matters if you stay on the sysctl path; current rc != 0 || len == 0 accepts a partially-populated buffer.

  • One smoke test for parent_of, cfg-gated, catches regressions on either platform without needing a wrapper-process integration test:

    #[cfg(any(target_os = \"linux\", target_os = \"macos\"))]
    #[test]
    fn parent_of_self_returns_some() {
        assert!(parent_of(std::process::id()).is_some());
    }

Docs

README expansion looks complete and well-structured. No CHANGELOG to maintain — consistent with the project so far.

Once the macOS path lands and the matrix goes green, this is a clean v0.2.0. Thanks again — really impressed by how cleanly each iteration converges.

@Filyus
Filyus force-pushed the feature/multi-session-router-telegram-topics branch from 05bdb88 to 20566c0 Compare April 25, 2026 21:50
@Filyus

Filyus commented Apr 25, 2026

Copy link
Copy Markdown
Contributor Author

Updated with the simpler macOS approach you recommended.
I replaced the temporary dependency-based fix with the lightweight ps lookup and added a small smoke test for the parent-walking path.

@maciek-hyperdev

Copy link
Copy Markdown
Contributor

Merging now — it ships as v0.2.0. 🎉

This is the largest external contribution to hdcd-telegram so far, and the quality bar stayed high the whole way through:

  • Coherent architecture — clean module boundaries (router/{config,mailbox,sessions,topics}), OS-level file lock for the mutex, peek + commit-after-send for at-least-once outbox semantics, reconcile-on-startup for orphans, idle auto-shutdown.
  • Session tracking beyond the original scope — parent-walk + ~/.claude/sessions/{pid}.json, with topic rebinding on --resume / /resume / /clear. That orphaned-topic problem was a real pain point and you solved it cleanly.
  • Every review item addressed across two roundslibc declaration + CI matrix, fail-fast on missing prereqs, i64 supergroup_id, fs_perms 0700/0600, allowlist invariant, token fallback, the is_pid_alive(u32::MAX) blocker, and the macOS parent_of rewrite to ps + smoke test — each turned around within hours.

Cross-platform CI (Linux / macOS / Windows) is fully green. Thank you, @Filyus — first-rate work, and exactly the kind of contribution that makes an OSS project better than its maintainers could manage alone. Looking forward to whatever you pick up next. 🙏

@maciek-hyperdev
maciek-hyperdev merged commit 2cd117d into gohyperdev:main May 24, 2026
8 checks passed
maciek-hyperdev added a commit that referenced this pull request May 24, 2026
Multi-session router (#2, @Filyus) — opt-in via --router, routes multiple
Claude Code sessions through one bot via Telegram forum topics, with session
tracking across --resume/--clear.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

2 participants