Bot bridge: mobile session management for all IM channels (conversation continuity, chat commands, per-conversation working directory)
- Dominant language
- TypeScript
- Stars
- 5.4k
- Forks
- 502
- Avg merge
- 1d 2h
- Merged PRs (30d)
- 715
Description
## Problem
Users of the bot bridge (WeChat, Telegram, Feishu, WeCom, Discord, DingTalk, QQ, Slack) cannot tell — let alone control — where a conversation works. A common first impression on WeChat: "I can't figure out how to point a conversation at a folder. Can it work like a local session — start inside a folder and understand its context?"
Looking deeper, the gap is structural, not just the folder. The bridge today is a dumb pipe: a message goes in, a reply comes out. Everything around it is missing:
- **No conversation continuity**: the conversation → session mapping in `bot-incoming-main.ts` (`${platform}:${chatId}` → sessionId) is an in-memory `Map`. Restart Maka and every bot conversation silently starts over in a fresh session.
- **No per-conversation working directory**: bot sessions are created with `resolveBotCreateTarget() → { cwd: await projectRoot.current() }` — the project last selected on the desktop, shared by every conversation on every channel. There is no way to bind a conversation to a folder, or even to see which folder it works in.
- **No management commands**: only a bare `restart`/`help` keyword. No way to list conversations, start a new one, stop a running turn, or check status. (`/stop` doesn't exist at all.)
- **Hardcoded explore permissions**: bot sessions are forced to `permissionMode: 'explore'`, so the agent cannot take write actions from any IM channel.
The issue is not "how to pick a folder" — it's that **IM conversations are not a session-management surface**. Local sessions get a project picker, a sidebar, model selection, and permission prompts; a bot conversation gets none of that. Compare OpenClaw's Control UI and Hermes' chat commands: both expose conversation management (resume, new, stop, status, working directory, approvals) over IM.
## Desired outcome
Turn the bot bridge into a mobile session-management surface, uniform across all connected channels, with the local session machinery as the single implementation:
- **Conversation continuity (P0)** — persist the `conversationKey → sessionId` mapping (Hermes `sessions.json` / PawWork `SessionPointers` / OpenClaw SQLite are the reference shapes). A conversation resumes its session across restarts, and any directory/model attributes survive with it.
- **Chat command framework (P0)** — `/new` (fresh session), `/sessions` (list/resume), `/stop` (interrupt the running turn), `/status` (current session, working directory, model). Implemented once in the platform-agnostic bot layer (`bot-incoming-main.ts`), so every channel — WeChat, Telegram, Feishu, WeCom, Discord, DingTalk, QQ, Slack — inherits it.
- **Per-conversation working directory (P0)** — the directory is a *session attribute*, exactly like a local session: chosen at creation (defaulting to the configurable default working directory, see #2322), changeable from a desktop-side conversation management entry (folder picker), visible in `/status` and in the sidebar grouping. The bridge only passes the directory through at session creation — it does not implement any folder logic of its own.
- **Permission fix + in-chat approval (P0)** — bot sessions follow the global default permission mode (`auto`/`bypass`) instead of being forced to `explore`. In `auto` mode, permission requests render as chat text and are answered by keywords (PawWork's blocker pattern: 是/好/允许/同意). Native buttons (Hermes `send_exec_approval`) are a later iteration, on channels that support them.
- **Later directions** — inbound media enrichment (image → vision, voice → STT; both Hermes and OpenClaw have reference implementations). Model selection is deliberately out of scope (covered by subagent capability).
## Alternatives or workarounds
No workaround today. We compared three open-source bridges:
| Dimension | PawWork remote-bridge | Hermes gateway | OpenClaw channels |
|---|---|---|---|
| Layering | Platform (protocol) → Engine (routing) → Gateway (supervision); thin adapters | `BasePlatformAdapter` normalizes to `MessageEvent`; Runner owns everything else | `ChannelPlugin` contract; core is channel-agnostic |
| Conversation identity | `::` key, persisted `SessionPointers` (atomic, root-conflict check) | Deterministic key `agent:main::dm:`, persisted `sessions.json`, `resume_pending` recovery | Deterministic `agent::::`, per-agent SQLite |
| Working directory | `createSession()` has **no directory param** → all IM sessions land in hardcoded `~/PawWork` (lesson: the seam must accept a directory from day one) | Channels share one global cwd; per-session cwd machinery exists (contextvar, `explicit_cwd`, only explicit dirs persist) | **Bind at session creation, persist on the session** (`spawnedCwd`), reused every turn |
| Commands / approvals | `/new` `/sessions` `/stop` `/help`; blockers as chat text answered by keywords | `/new` `/reset` `/stop` `/sessions`; button-based model picker, clarify, exec approval | `/new` + customCommands; native approval buttons where the channel supports them |
Conclusions:
1. **Keep the bridge thin** — adapters normalize I/O only; sessions, directories, commands, and permissions live in the core (all three projects agree).
2. **Persist the conversation mapping** with a deterministic key — restart recovery becomes free.
3. **Bind the directory at session creation and persist it as a session attribute** — never resolve it per message, and never build a separate bot-side directory model. Maka's existing seam (`resolveBotCreateTarget`) is the single place to extend.
4. **Caveats are per-channel**: PawWork's missing directory param silently parked every IM session in `~/PawWork`. WeChat iLink has no server push (replies echo `context_token`) and one credential = one poller (409) — commands must work as replies to inbound messages; other channels bring their own constraints (e.g. Telegram's single-getUpdates consumer).
中文对照(简体中文)
## 问题
bot 桥的用户(微信、Telegram、飞书、企业微信、Discord、钉钉、QQ、Slack)既说不清、也控制不了对话到底在哪个文件夹里干活。微信上常见的第一印象是:"我没弄懂怎么指定对话所在的文件夹。能不能像本地对话一样,直接在某个文件夹下对话,理解文件夹的上下文?"
往深看,缺口是结构性的,不只是文件夹的问题。现在的桥接器只是一根"哑管道":消息进、回复出,周围该有的东西全都没有:
- **对话没有连续性**:`bot-incoming-main.ts` 里的对话 → 会话映射(`${platform}:${chatId}` → sessionId)只是内存里的 `Map`。重启 Maka 后,每个 bot 对话都会悄悄地在全新会话里重新开始。
- **没有按对话区分的工作目录**:bot 会话创建时 `resolveBotCreateTarget() → { cwd: await projectRoot.current() }`——用的是桌面端最后选中的项目,所有渠道的所有对话共享。既不能把某个对话绑定到某个文件夹,甚至看不到它在哪个文件夹干活。
- **没有管理命令**:只有裸的 `restart`/`help` 关键词。不能列出会话、不能开新会话、不能中断进行中的回合、不能查状态。(`/stop` 压根不存在。)
- **权限被写死成 explore**:bot 会话被强制 `permissionMode: 'explore'`,agent 在任意 IM 渠道里完全不能执行写操作。
问题不在于"怎么选文件夹",而在于**IM 对话不是一个会话管理面**。本地会话有项目选择器、侧栏、模型选择、权限提示;bot 对话这些全都没有。反观 OpenClaw 的 Control UI 和 Hermes 的聊天命令:两者都把会话管理能力(续聊、新建、停止、状态、工作目录、审批)暴露到了 IM 里。
## 期望结果
把 bot 桥变成手机端的会话管理面,对所有已接入渠道统一生效,并且以本地会话机制为唯一实现:
- **对话连续性(P0)**——持久化 `conversationKey → sessionId` 映射(参考形态:Hermes `sessions.json` / PawWork `SessionPointers` / OpenClaw SQLite)。对话跨重启续上原会话,绑定的目录、模型等属性随之保留。
- **聊天命令框架(P0)**——`/new`(开新会话)、`/sessions`(列出/续聊)、`/stop`(中断当前回合)、`/status`(当前会话、工作目录、模型)。在平台无关的 bot 层(`bot-incoming-main.ts`)实现一次,所有渠道——微信、Telegram、飞书、企业微信、Discord、钉钉、QQ、Slack——自然继承。
- **按对话的工作目录(P0)**——目录是**会话属性**,和本地会话完全一致:创建时选定(默认落到可配置的默认工作目录,见 #2322),可在桌面端的对话管理入口(文件夹选择器)修改,在 `/status` 和侧栏分组里可见。桥接器只在建会话时把目录传进去,自身不实现任何文件夹逻辑。
- **权限修正 + 聊天内审批(P0)**——bot 会话跟随全局默认权限模式(`auto`/`bypass`),不再强制 `explore`。`auto` 模式下,权限请求渲染成聊天文本,用关键词回复(PawWork 的 blocker 模式:是/好/允许/同意)。原生按钮(Hermes `send_exec_approval`)留到后续迭代,且只用于支持的渠道。
- **后续方向**——入站媒体富化(图片 → vision、语音 → STT;Hermes 和 OpenClaw 都有现成实现可参考)。模型选择有意不做(由 subagent 能力覆盖)。
## 替代方案或变通
今天没有任何变通。我们对比了三个开源桥接实现:
| 维度 | PawWork remote-bridge | Hermes gateway | OpenClaw channels |
|---|---|---|---|
| 分层 | Platform(协议)→ Engine(路由)→ Gateway(监督);适配器很薄 | `BasePlatformAdapter` 归一化为 `MessageEvent`;其余全归 Runner | `ChannelPlugin` 契约;核心与渠道无关 |
| 会话身份 | `::` key,`SessionPointers` 持久化(原子写、根冲突检查) | 确定性 key `agent:main::dm:`,`sessions.json` 持久化,`resume_pending` 恢复 | 确定性 `agent::::`,按 agent 存 SQLite |
| 工作目录 | `createSession()` **没有目录参数** → 所有 IM 会话落进硬编码 `~/PawWork`(教训:接缝第一天就要接受目录) | 渠道共享一个全局 cwd;per-session cwd 机制已存在(contextvar、`explicit_cwd`,只有显式选择的目录才落库) | **建会话时绑定、持久化在会话上**(`spawnedCwd`),每轮沿用 |
| 命令/审批 | `/new` `/sessions` `/stop` `/help`;blocker 以聊天文本呈现、关键词回复 | `/new` `/reset` `/stop` `/sessions`;按钮化模型选择、clarify、exec 审批 | `/new` + customCommands;渠道支持时用原生审批按钮 |
结论:
1. **桥接器保持薄**——适配器只做消息收发归一化;会话、目录、命令、权限都放核心层(三家一致)。
2. **用确定性 key 持久化对话映射**——重启恢复零成本。
3. **建会话时绑定目录、作为会话属性持久化**——绝不在消息级别临时解析,也绝不做一套 bot 侧的目录模型。Maka 现有的接缝(`resolveBotCreateTarget`)就是唯一需要扩展的地方。
4. **踩坑按渠道各有各的**:PawWork 因为少了目录参数,所有 IM 会话静默落进 `~/PawWork`。微信 iLink 没有服务端推送(回复必须回显 `context_token`)、一份凭据只能一个轮询者(409)——命令必须作为入站消息的回复来工作;其他渠道也有各自的约束(比如 Telegram 的单一 getUpdates consumer)。
Contributor guide
Assessment
This issue has not been assessed yet.