grok-telegram-bot 2.5.0 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +71 -0
- package/README.md +5 -3
- package/docs/GROUP.md +39 -4
- package/package.json +4 -4
- package/src/app/types.ts +27 -1
- package/src/bot/auth.ts +5 -1
- package/src/bot/bot.ts +73 -4
- package/src/bot/chat-controller.ts +129 -0
- package/src/bot/group-memory.ts +192 -12
- package/src/bot/handlers/forum.ts +16 -6
- package/src/bot/handlers/message.ts +165 -25
- package/src/bot/handlers/photo.ts +4 -1
- package/src/bot/image-return.ts +4 -1
- package/src/bot/manager-context.ts +208 -0
- package/src/bot/manager-jobs.ts +142 -0
- package/src/bot/menu/ephemeral.ts +4 -1
- package/src/bot/prompt-anchor.ts +2 -3
- package/src/bot/prompt-content.ts +5 -0
- package/src/bot/scope.ts +9 -8
- package/src/bot/session-runtime.ts +663 -55
- package/src/bot/telegram-actions.ts +728 -38
- package/src/bot/telegram-bots.ts +2 -1
- package/src/bot/telegram-io.ts +4 -1
- package/src/forum/manager.ts +2 -1
- package/src/forum/thread.ts +33 -0
- package/src/render/manager-directive.ts +137 -0
- package/src/render/session-comment.ts +10 -0
- package/src/render/telegram-bridge.ts +118 -14
- package/src/sessions/history.ts +18 -0
- package/src/stream/streamer.ts +46 -10
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,76 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
7
7
|
The latest section is published verbatim as the GitHub Release notes by
|
|
8
8
|
`.github/workflows/release.yml` when a `vX.Y.Z` tag is pushed.
|
|
9
9
|
|
|
10
|
+
## [2.6.0] - 2026-08-30
|
|
11
|
+
|
|
12
|
+
General becomes a chat-like **manager** for the forum group: memory-first
|
|
13
|
+
routing, `send_prompt` into project topics with job report-back, and **visible
|
|
14
|
+
short replies** (the previous notify-only quiet path looked like the bot was
|
|
15
|
+
dead). Also bumps runtime deps (`tsx`, `grammy`).
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- **General manager mode.** The forum **General** topic (`thread id 1`) is an
|
|
20
|
+
OpenClaw-style orchestrator, not a coding workspace. It keeps user messages,
|
|
21
|
+
starts a **new parallel session** per message (reply-to continues the same
|
|
22
|
+
session), streams **prose only** (no tools / progress / Done spam), and
|
|
23
|
+
dispatches real work into project topics.
|
|
24
|
+
- **Manager directive + auto context.** First General prompt injects how to
|
|
25
|
+
behave (delegate, memory-first, no git-first). Every user turn also gets a
|
|
26
|
+
capped **MANAGER CONTEXT** block: topic catalog, recent General chat, ranked
|
|
27
|
+
memory hits, and in-process manager jobs.
|
|
28
|
+
- **Manager jobs + work reports.** `send_prompt` from General registers a job
|
|
29
|
+
(`list_jobs`). When the child topic turn finishes, General is woken with a
|
|
30
|
+
structured `MANAGER WORK REPORT` so the manager can tell you done / failed.
|
|
31
|
+
- **Bridge: `notify`, `list_topics`, `list_jobs`, `session_id`.**
|
|
32
|
+
- `notify` — optional extra ping (not required in General; prose is shown).
|
|
33
|
+
- `list_topics` / `list_jobs` — catalog and dispatch status for the agent.
|
|
34
|
+
- `send_prompt.session_id` — resume a **specific** Grok session (full UUID or
|
|
35
|
+
short prefix from memory). Topic may be omitted; path → forum topic is
|
|
36
|
+
inferred. Placeholder topics (`…`) are rejected / inferred from memory.
|
|
37
|
+
- **Group memory ranking.** `search_memory` scores **relevance + recency**
|
|
38
|
+
(newest session per project path wins) so “last modifications” does not
|
|
39
|
+
surface stale Done notes. Hits include `[Xm/h/d ago]` stamps.
|
|
40
|
+
- **Outbound General thread helper.** Telegram rejects `message_thread_id: 1`.
|
|
41
|
+
Sends/edits to General omit that field (`outboundThreadExtra`); inbound
|
|
42
|
+
routing still treats missing thread id as General.
|
|
43
|
+
|
|
44
|
+
### Changed
|
|
45
|
+
|
|
46
|
+
- **General UX.** User messages are **kept** (not replaced by prompt anchors).
|
|
47
|
+
Replies thread to the user message. A `Starting…` / `Thinking…` bubble is
|
|
48
|
+
edited in place into the short answer. Independent General messages no longer
|
|
49
|
+
coalesce into one queued turn.
|
|
50
|
+
- **Prose is the user-facing channel.** The manager is told the user **sees**
|
|
51
|
+
free-form chat text. `notify` is optional. Bridge-result follow-ups (after
|
|
52
|
+
`search_memory` / `list_topics`) stream the real answer instead of staying
|
|
53
|
+
silent. If the model emits no prose, the Thinking bubble is replaced with a
|
|
54
|
+
short fallback — never deleted leaving an empty chat.
|
|
55
|
+
- **Streamer prose-only mode.** General streams agent text only (no thoughts,
|
|
56
|
+
tools, plan board, or progress bar), seeded on the Thinking placeholder.
|
|
57
|
+
- **Forum send path.** `sendMessage` / stream edits / auth deny / prompt
|
|
58
|
+
anchors / photos / docs use `outboundThreadExtra` so General posts succeed.
|
|
59
|
+
- **Dependencies.** `tsx` `^4.19.2` → `^4.23.13`; `grammy` and `@types/node`
|
|
60
|
+
updated to current 1.x / 22.x wanted versions.
|
|
61
|
+
|
|
62
|
+
### Fixed
|
|
63
|
+
|
|
64
|
+
- **Bot not responding in General after manager-mode work.** Notify-only +
|
|
65
|
+
deleting `Thinking…` when `search_memory` chained made every answer vanish.
|
|
66
|
+
General now streams short prose, keeps the status bubble across bridge
|
|
67
|
+
follow-ups, and never drops the placeholder without a visible line.
|
|
68
|
+
- **`message_thread_id: 1` rejected by Telegram** (“message thread not found”)
|
|
69
|
+
on General sends — omit the field for General; keep it for real topics.
|
|
70
|
+
- **Manager context path trap.** Recent General history no longer treats every
|
|
71
|
+
session under the workspace root as General chat (title + exact cwd only).
|
|
72
|
+
|
|
73
|
+
### Docs
|
|
74
|
+
|
|
75
|
+
- **[docs/GROUP.md](./docs/GROUP.md)** — General = manager, memory-first
|
|
76
|
+
dispatch, `session_id`, `list_jobs`, prose vs `notify`.
|
|
77
|
+
- **AGENTS.md** — General manager conventions for future changes.
|
|
78
|
+
- **README** — General manager + expanded bridge actions.
|
|
79
|
+
|
|
10
80
|
## [2.5.0] - 2026-08-05
|
|
11
81
|
|
|
12
82
|
Forum project topics, cross-topic Telegram bridge, prompt anchors, and a more
|
|
@@ -931,6 +1001,7 @@ from a single chat and switch between them, on a redesigned, compact menu.
|
|
|
931
1001
|
diffs, MarkdownV2 rendering, scheduled tasks, multi-image prompts, and a
|
|
932
1002
|
cross-platform 24/7 background service.
|
|
933
1003
|
|
|
1004
|
+
[2.6.0]: https://github.com/artickc/grok-telegram-bot/releases/tag/v2.6.0
|
|
934
1005
|
[2.5.0]: https://github.com/artickc/grok-telegram-bot/releases/tag/v2.5.0
|
|
935
1006
|
[2.4.0]: https://github.com/artickc/grok-telegram-bot/releases/tag/v2.4.0
|
|
936
1007
|
[2.3.1]: https://github.com/artickc/grok-telegram-bot/releases/tag/v2.3.1
|
package/README.md
CHANGED
|
@@ -27,8 +27,9 @@ re-architected for the Grok Build CLI and extended into a full multi-session cli
|
|
|
27
27
|
| Capability | What it does |
|
|
28
28
|
|---|---|
|
|
29
29
|
| 🗂 **Projects** | `/projects` browses your folders and runs Grok in the one you pick. |
|
|
30
|
-
| 🏷 **Forum project topics** | Optional forum supergroup:
|
|
31
|
-
|
|
|
30
|
+
| 🏷 **Forum project topics** | Optional forum supergroup: **General** manager, **AI Chat** workspace, one topic per project, exact-name bind, `/forum_setup`. See **[docs/GROUP.md](./docs/GROUP.md)**. |
|
|
31
|
+
| 🎛 **General manager** | Forum General is a chat-like orchestrator: memory-first, short replies, `send_prompt` into project topics, report-back when child work finishes. |
|
|
32
|
+
| 🌉 **Telegram bridge** | Agent can `create_topic`, `set_path`, `send_prompt` (optional `session_id`), `notify`, `search_memory`, `list_topics` / `list_jobs`, and call allowlisted sibling bots via JSON actions. |
|
|
32
33
|
| 🔖 **Prompt anchors** | Each user prompt is re-posted with a searchable `#prompt_…` tag; stream/Done messages reply to that anchor (media re-attached). |
|
|
33
34
|
| 🆕 **New session** | `/new` or the bar **New** button starts a fresh session; forum topics keep New on the topic menu. |
|
|
34
35
|
| 💡 **Post-turn suggestions** | After Done, 1–3 scored follow-ups as buttons; high-score items can auto-queue as one multi-step prompt. |
|
|
@@ -66,7 +67,8 @@ re-architected for the Grok Build CLI and extended into a full multi-session cli
|
|
|
66
67
|
| Connect Grok CLI to Telegram (ACP) | ✅ | ✅ |
|
|
67
68
|
| Switch between projects | ✅ | ❌ |
|
|
68
69
|
| **Forum topics = projects** + AI Chat workspace | ✅ | ❌ |
|
|
69
|
-
| **
|
|
70
|
+
| **General manager** (memory-first, dispatch + report-back) | ✅ | ❌ |
|
|
71
|
+
| **Cross-topic agent bridge** (create / bind / send_prompt / session_id) | ✅ | ❌ |
|
|
70
72
|
| Resume saved sessions | ✅ | ❌ |
|
|
71
73
|
| Attach to **live** PC sessions (watch / fork) | ✅ | ❌ |
|
|
72
74
|
| **Kill a session by PID** (or all at once) | ✅ | ❌ |
|
package/docs/GROUP.md
CHANGED
|
@@ -67,11 +67,24 @@ When the bot is later promoted to admin, it re-probes automatically (`my_chat_me
|
|
|
67
67
|
|
|
68
68
|
| Topic | Working directory | Typical use |
|
|
69
69
|
|---|---|---|
|
|
70
|
-
| **General**
|
|
70
|
+
| **General** | `GROK_WORKSPACE` | **Manager chat** (OpenClaw-style): routes work, memory-first, no coding spam |
|
|
71
|
+
| **AI Chat** | `GROK_WORKSPACE` | Normal coding/conversation in the workspace |
|
|
71
72
|
| **Project topic** | Bound project path | All coding work for that folder |
|
|
72
73
|
| **User-created topic** | Bound after name/path match | Ad-hoc projects or new folders |
|
|
73
74
|
|
|
74
|
-
|
|
75
|
+
### General = manager
|
|
76
|
+
|
|
77
|
+
Messages in **General** drive a chat-like orchestrator, not a coding agent:
|
|
78
|
+
|
|
79
|
+
1. User asks in General (e.g. “fix login in MyApp”).
|
|
80
|
+
2. Bot uses **memory + topic catalog** (auto-injected) and may call `search_memory` / `list_topics`.
|
|
81
|
+
3. It replies briefly (“OK — I’ll start … in **MyApp**”) with **no progress bars / tool dumps**.
|
|
82
|
+
4. It **dispatches** via `create_topic` / `set_path` / `send_prompt` into the right project topic.
|
|
83
|
+
5. When that child turn finishes, the bridge **wakes General** with a `MANAGER WORK REPORT` so the manager summarizes success/fail for you.
|
|
84
|
+
|
|
85
|
+
Real implementation stays in **project topics**. General should not edit app code.
|
|
86
|
+
|
|
87
|
+
Messages you send **inside a project topic** are prompts for a session whose `cwd` is
|
|
75
88
|
that topic’s path. Menus, model/reasoning picks, `/sessions`, `/running`, and
|
|
76
89
|
Stop are **topic-scoped** so one project does not steal another’s session.
|
|
77
90
|
|
|
@@ -148,8 +161,11 @@ may feed results back as a quiet system turn (not a second Done).
|
|
|
148
161
|
|---|---|
|
|
149
162
|
| `create_topic` | New forum topic; optional `path` binds immediately |
|
|
150
163
|
| `set_path` | Bind/rebind topic by title or `#threadId` |
|
|
151
|
-
| `send_prompt` | Inject a prompt into another topic (`ran` / `queued`; optional `new_session
|
|
152
|
-
| `
|
|
164
|
+
| `send_prompt` | Inject a prompt into another topic (`ran` / `queued`; optional `new_session`, **`session_id`** to resume a specific session) |
|
|
165
|
+
| `notify` | Optional extra ping. In **General** the user already sees short chat prose; `notify` is not required. |
|
|
166
|
+
| `search_memory` | Search topic + session indexes (memory-first) |
|
|
167
|
+
| `list_topics` | List mapped forum topics (name, `#id`, path) |
|
|
168
|
+
| `list_jobs` | List recent General → project dispatches |
|
|
153
169
|
| `list_bots` | List allowlisted sibling bots |
|
|
154
170
|
| `bot_command` | Call `/command@bot` and wait for that bot to settle |
|
|
155
171
|
|
|
@@ -174,6 +190,25 @@ Example (from General / AI Chat):
|
|
|
174
190
|
|
|
175
191
|
`topic` may be the exact title, `#threadId`, `general`, or `ai chat`.
|
|
176
192
|
|
|
193
|
+
To **resume a related session** found via memory (not the topic's currently open session):
|
|
194
|
+
|
|
195
|
+
```json
|
|
196
|
+
{
|
|
197
|
+
"telegram": [
|
|
198
|
+
{
|
|
199
|
+
"action": "send_prompt",
|
|
200
|
+
"topic": "MyApp",
|
|
201
|
+
"session_id": "019fc9ec",
|
|
202
|
+
"prompt": "Continue: apply the follow-up fix."
|
|
203
|
+
}
|
|
204
|
+
]
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
`session_id` may be a full UUID or a short prefix from memory hits. Without it, the bridge uses the topic's foreground session.
|
|
209
|
+
|
|
210
|
+
`topic` must be an **exact** mapped title or `#threadId` (never placeholders like `…`). If only the session is known, omit `topic` — the bridge maps `session.cwd` → forum topic.
|
|
211
|
+
|
|
177
212
|
---
|
|
178
213
|
|
|
179
214
|
## Sibling bots (optional)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "grok-telegram-bot",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.0",
|
|
4
4
|
"description": "Control the official Grok Build CLI from Telegram over the Agent Client Protocol (ACP). Sign in with your xAI account, switch projects, resume sessions, stream responses with diffs, queue follow-ups, manage multiple sign-ins, and run 24/7 as a cross-platform background service.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -60,15 +60,15 @@
|
|
|
60
60
|
"dependencies": {
|
|
61
61
|
"diff": "^7.0.0",
|
|
62
62
|
"dotenv": "^16.4.7",
|
|
63
|
-
"grammy": "^1.
|
|
64
|
-
"tsx": "^4.
|
|
63
|
+
"grammy": "^1.46.0",
|
|
64
|
+
"tsx": "^4.23.13"
|
|
65
65
|
},
|
|
66
66
|
"optionalDependencies": {
|
|
67
67
|
"@homebridge/node-pty-prebuilt-multiarch": "0.13.1"
|
|
68
68
|
},
|
|
69
69
|
"devDependencies": {
|
|
70
70
|
"@types/diff": "^7.0.0",
|
|
71
|
-
"@types/node": "^22.
|
|
71
|
+
"@types/node": "^22.20.1",
|
|
72
72
|
"typescript": "^5.7.2"
|
|
73
73
|
}
|
|
74
74
|
}
|
package/src/app/types.ts
CHANGED
|
@@ -78,13 +78,37 @@ export interface PromptInput {
|
|
|
78
78
|
* trigger another self-recheck — only real user prompts do (once each).
|
|
79
79
|
*/
|
|
80
80
|
skipSelfRecheck?: boolean;
|
|
81
|
+
/**
|
|
82
|
+
* Manager dispatch metadata for this prompt only (General → project).
|
|
83
|
+
* Carried through the queue so concurrent send_prompt jobs do not steal
|
|
84
|
+
* each other's report-back. Shape matches bot/manager-jobs ReportBackMeta.
|
|
85
|
+
*/
|
|
86
|
+
reportBack?: {
|
|
87
|
+
jobId: string;
|
|
88
|
+
originChatId: number;
|
|
89
|
+
originThreadId: number;
|
|
90
|
+
userAskPreview: string;
|
|
91
|
+
targetName: string;
|
|
92
|
+
targetPath: string;
|
|
93
|
+
dispatchPrompt: string;
|
|
94
|
+
};
|
|
95
|
+
/**
|
|
96
|
+
* Pre-posted status bubble (General: "Starting…") that the turn edits to
|
|
97
|
+
* "Thinking…" then streams the agent reply into.
|
|
98
|
+
*/
|
|
99
|
+
seedMessageId?: number;
|
|
81
100
|
}
|
|
82
101
|
|
|
83
102
|
export function textPrompt(
|
|
84
103
|
text: string,
|
|
85
104
|
replyTo?: number,
|
|
86
105
|
quotedText?: string,
|
|
87
|
-
opts?: {
|
|
106
|
+
opts?: {
|
|
107
|
+
skipSelfRecheck?: boolean;
|
|
108
|
+
promptId?: string;
|
|
109
|
+
reportBack?: PromptInput["reportBack"];
|
|
110
|
+
seedMessageId?: number;
|
|
111
|
+
},
|
|
88
112
|
): PromptInput {
|
|
89
113
|
return {
|
|
90
114
|
text,
|
|
@@ -94,5 +118,7 @@ export function textPrompt(
|
|
|
94
118
|
promptId: opts?.promptId,
|
|
95
119
|
quotedText,
|
|
96
120
|
skipSelfRecheck: opts?.skipSelfRecheck,
|
|
121
|
+
reportBack: opts?.reportBack,
|
|
122
|
+
seedMessageId: opts?.seedMessageId,
|
|
97
123
|
};
|
|
98
124
|
}
|
package/src/bot/auth.ts
CHANGED
|
@@ -112,7 +112,11 @@ async function denyUnauthorized(
|
|
|
112
112
|
}
|
|
113
113
|
if (!ctx.chat || isGroupChat(ctx)) return; // groups: ignore quietly
|
|
114
114
|
const threadId = m && "message_thread_id" in m ? m.message_thread_id : undefined;
|
|
115
|
-
|
|
115
|
+
// Omit General (1) — Bot API rejects message_thread_id=1.
|
|
116
|
+
const extra =
|
|
117
|
+
threadId !== undefined && threadId !== 1
|
|
118
|
+
? { message_thread_id: threadId as number }
|
|
119
|
+
: {};
|
|
116
120
|
await ctx
|
|
117
121
|
.reply("\u26D4 Not authorized. Ask the bot owner to add your Telegram ID to ALLOWED_USERS.", extra)
|
|
118
122
|
.catch(() => {});
|
package/src/bot/bot.ts
CHANGED
|
@@ -116,21 +116,58 @@ export async function createBot(cfg: AppConfig, acp: GrokClient): Promise<BotBun
|
|
|
116
116
|
forum,
|
|
117
117
|
bots: telegramBots,
|
|
118
118
|
// Cross-topic orchestration: General can create a topic and send_prompt there.
|
|
119
|
-
|
|
119
|
+
// sessionId resumes a specific Grok session (memory follow-up) instead of
|
|
120
|
+
// dumping into whatever is currently open in that topic.
|
|
121
|
+
submitTopicPrompt: async ({
|
|
122
|
+
threadId,
|
|
123
|
+
cwd,
|
|
124
|
+
projectName,
|
|
125
|
+
prompt,
|
|
126
|
+
newSession,
|
|
127
|
+
sessionId,
|
|
128
|
+
reportBack,
|
|
129
|
+
}) => {
|
|
120
130
|
if (cfg.topicGroupId === undefined) {
|
|
121
131
|
throw new Error("TOPIC_GROUP_ID unset");
|
|
122
132
|
}
|
|
123
133
|
const groupId = cfg.topicGroupId;
|
|
124
134
|
const controller = registry.forumController(groupId, threadId, cwd, projectName);
|
|
135
|
+
// Explicit resume wins over newSession / foreground.
|
|
136
|
+
if (sessionId) {
|
|
137
|
+
const sw = await controller.addResume(sessionId, cwd, projectName);
|
|
138
|
+
if (reportBack) sw.rt.setReportBack(reportBack);
|
|
139
|
+
const outcome = await sw.rt.submit(textPrompt(prompt));
|
|
140
|
+
return { outcome, sessionId: sw.rt.sessionId ?? sessionId };
|
|
141
|
+
}
|
|
125
142
|
if (newSession) {
|
|
126
143
|
const rt = await controller.addNew(cwd, projectName);
|
|
144
|
+
if (reportBack) rt.setReportBack(reportBack);
|
|
127
145
|
const outcome = await rt.submit(textPrompt(prompt));
|
|
128
146
|
return { outcome, sessionId: rt.sessionId };
|
|
129
147
|
}
|
|
130
148
|
const rt = controller.foreground();
|
|
149
|
+
if (reportBack) rt.setReportBack(reportBack);
|
|
131
150
|
const outcome = await rt.submit(textPrompt(prompt));
|
|
132
151
|
return { outcome, sessionId: rt.sessionId };
|
|
133
152
|
},
|
|
153
|
+
// Child topic Done → wake General manager with a WORK REPORT.
|
|
154
|
+
wakeManager: async ({ originChatId, originThreadId, prompt }) => {
|
|
155
|
+
if (cfg.topicGroupId === undefined) {
|
|
156
|
+
throw new Error("TOPIC_GROUP_ID unset");
|
|
157
|
+
}
|
|
158
|
+
const groupId = cfg.topicGroupId;
|
|
159
|
+
// origin is always General in manager mode; bind workspace.
|
|
160
|
+
const controller = registry.forumController(
|
|
161
|
+
originChatId || groupId,
|
|
162
|
+
originThreadId,
|
|
163
|
+
cfg.workspace,
|
|
164
|
+
"General",
|
|
165
|
+
);
|
|
166
|
+
const rt = controller.foreground();
|
|
167
|
+
await rt.submit(
|
|
168
|
+
textPrompt(prompt, undefined, undefined, { skipSelfRecheck: true }),
|
|
169
|
+
);
|
|
170
|
+
},
|
|
134
171
|
});
|
|
135
172
|
|
|
136
173
|
const deps: BotDeps = {
|
|
@@ -231,18 +268,50 @@ export async function createBot(cfg: AppConfig, acp: GrokClient): Promise<BotBun
|
|
|
231
268
|
const index = Number(ctx.match![2]);
|
|
232
269
|
const { resolveScope } = await import("./scope.js");
|
|
233
270
|
const { adoptUserPrompt } = await import("./prompt-anchor.js");
|
|
271
|
+
const { isGeneralThread } = await import("../forum/thread.js");
|
|
234
272
|
const scope = resolveScope(ctx, deps);
|
|
235
|
-
|
|
236
|
-
const
|
|
237
|
-
|
|
273
|
+
// General may have parallel sessions — find the runtime that owns this batch.
|
|
274
|
+
const hit =
|
|
275
|
+
scope.controller.takeSuggestionAnywhere(batchId, index) ??
|
|
276
|
+
(() => {
|
|
277
|
+
const t = scope.rt.takeSuggestion(batchId, index);
|
|
278
|
+
return t ? { rt: scope.rt, text: t } : undefined;
|
|
279
|
+
})();
|
|
280
|
+
if (!hit) {
|
|
238
281
|
await ctx.answerCallbackQuery({ text: "Suggestion expired", show_alert: true });
|
|
239
282
|
return;
|
|
240
283
|
}
|
|
284
|
+
const { rt, text } = hit;
|
|
241
285
|
await ctx.answerCallbackQuery({ text: "Sending\u2026" });
|
|
242
286
|
// Dim the keyboard so double-taps don't re-fire.
|
|
243
287
|
await ctx.editMessageReplyMarkup({ reply_markup: { inline_keyboard: [] } }).catch(() => {});
|
|
244
288
|
try {
|
|
245
289
|
const chatId = ctx.chat?.id;
|
|
290
|
+
const isGeneral = isGeneralThread(scope.threadId);
|
|
291
|
+
const sugMsgId = ctx.callbackQuery.message?.message_id;
|
|
292
|
+
// General: keep chat clean — no anchor overwrite; continue same session
|
|
293
|
+
// and reply to the message that carried the buttons.
|
|
294
|
+
if (isGeneral) {
|
|
295
|
+
if (rt.sessionId && sugMsgId !== undefined) {
|
|
296
|
+
scope.controller.bindTelegramMessage(sugMsgId, rt.sessionId);
|
|
297
|
+
}
|
|
298
|
+
const outcome = await rt.submit(
|
|
299
|
+
textPrompt(text, sugMsgId, undefined, { promptId: undefined }),
|
|
300
|
+
);
|
|
301
|
+
if (outcome === "queued") {
|
|
302
|
+
const extra: Record<string, unknown> = { ...scope.threadExtra };
|
|
303
|
+
if (sugMsgId !== undefined) {
|
|
304
|
+
extra.reply_parameters = {
|
|
305
|
+
message_id: sugMsgId,
|
|
306
|
+
allow_sending_without_reply: true,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
await deps.api
|
|
310
|
+
.sendMessage(chatId!, "\u{1F4E5} Queued on that thread.", extra)
|
|
311
|
+
.catch(() => {});
|
|
312
|
+
}
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
246
315
|
const anchor =
|
|
247
316
|
chatId !== undefined
|
|
248
317
|
? await adoptUserPrompt(deps.api, {
|
|
@@ -63,6 +63,12 @@ export class ChatController {
|
|
|
63
63
|
/** Telegram bridge (forum / memory / sibling bots); set by the registry. */
|
|
64
64
|
bridge?: ChatBridgeServices;
|
|
65
65
|
|
|
66
|
+
/**
|
|
67
|
+
* General manager: map Telegram message ids (user + bot) → session id so a
|
|
68
|
+
* reply-to continues the same ACP session instead of spawning a new one.
|
|
69
|
+
*/
|
|
70
|
+
private readonly telegramMsgSessions = new Map<number, string>();
|
|
71
|
+
|
|
66
72
|
constructor(
|
|
67
73
|
private readonly api: Api,
|
|
68
74
|
private readonly chatId: number,
|
|
@@ -128,6 +134,105 @@ export class ChatController {
|
|
|
128
134
|
return rt;
|
|
129
135
|
}
|
|
130
136
|
|
|
137
|
+
/**
|
|
138
|
+
* General manager: spawn a fresh session for one user message without
|
|
139
|
+
* killing an in-flight sibling's Telegram stream (parallel prompts).
|
|
140
|
+
*/
|
|
141
|
+
async addParallel(cwd: string, projectName?: string): Promise<SessionRuntime> {
|
|
142
|
+
return this.addNew(cwd, projectName);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Remember that a Telegram message belongs to a Grok session (reply routing). */
|
|
146
|
+
bindTelegramMessage(messageId: number, sessionId: string): void {
|
|
147
|
+
if (!messageId || !sessionId) return;
|
|
148
|
+
this.telegramMsgSessions.set(messageId, sessionId);
|
|
149
|
+
// Cap map growth (keep newest ~500).
|
|
150
|
+
if (this.telegramMsgSessions.size > 500) {
|
|
151
|
+
const drop = this.telegramMsgSessions.size - 500;
|
|
152
|
+
let i = 0;
|
|
153
|
+
for (const k of this.telegramMsgSessions.keys()) {
|
|
154
|
+
this.telegramMsgSessions.delete(k);
|
|
155
|
+
if (++i >= drop) break;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Resolve a controlled runtime from a Telegram message id (reply-to). */
|
|
161
|
+
runtimeForTelegramMessage(messageId: number | undefined): SessionRuntime | undefined {
|
|
162
|
+
if (messageId === undefined) return undefined;
|
|
163
|
+
this.ensureRestored();
|
|
164
|
+
const sid = this.telegramMsgSessions.get(messageId);
|
|
165
|
+
if (sid) {
|
|
166
|
+
const byId = this.runtimes.find((r) => r.sessionId === sid);
|
|
167
|
+
if (byId) return byId;
|
|
168
|
+
}
|
|
169
|
+
return undefined;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Resolve session from `#sess_xxxxxxxx` in a replied-to bot message body
|
|
174
|
+
* (works across process restarts when the in-memory map is cold).
|
|
175
|
+
*/
|
|
176
|
+
runtimeForSessionTag(text: string | undefined): SessionRuntime | undefined {
|
|
177
|
+
if (!text) return undefined;
|
|
178
|
+
this.ensureRestored();
|
|
179
|
+
const tag = parseSessTag(text);
|
|
180
|
+
if (!tag) return undefined;
|
|
181
|
+
return this.runtimes.find((r) => r.sessionId && sessionMatchesTag(r.sessionId, tag));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Cold-start / map-miss: resolve continue target from reply message id, #sess_
|
|
186
|
+
* tag on controlled runtimes, or disk session list (attach into this controller).
|
|
187
|
+
*/
|
|
188
|
+
async resolveContinueFromReply(opts: {
|
|
189
|
+
replyToMessageId?: number;
|
|
190
|
+
replyToText?: string;
|
|
191
|
+
cwd: string;
|
|
192
|
+
projectName?: string;
|
|
193
|
+
}): Promise<SessionRuntime | undefined> {
|
|
194
|
+
this.ensureRestored();
|
|
195
|
+
const byMsg = this.runtimeForTelegramMessage(opts.replyToMessageId);
|
|
196
|
+
if (byMsg) return byMsg;
|
|
197
|
+
const byTag = this.runtimeForSessionTag(opts.replyToText);
|
|
198
|
+
if (byTag) return byTag;
|
|
199
|
+
|
|
200
|
+
const tag = parseSessTag(opts.replyToText);
|
|
201
|
+
if (!tag) return undefined;
|
|
202
|
+
// Disk fallback: find full session id, then resume into this controller.
|
|
203
|
+
let metas;
|
|
204
|
+
try {
|
|
205
|
+
metas = this.store.list(80);
|
|
206
|
+
} catch {
|
|
207
|
+
return undefined;
|
|
208
|
+
}
|
|
209
|
+
const meta = metas.find((m) => sessionMatchesTag(m.sessionId, tag));
|
|
210
|
+
if (!meta) return undefined;
|
|
211
|
+
try {
|
|
212
|
+
const sw = await this.addResume(
|
|
213
|
+
meta.sessionId,
|
|
214
|
+
meta.cwd || opts.cwd,
|
|
215
|
+
opts.projectName,
|
|
216
|
+
);
|
|
217
|
+
return sw.rt;
|
|
218
|
+
} catch {
|
|
219
|
+
return undefined;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** Find any runtime that still holds this suggestion batch (General parallel). */
|
|
224
|
+
takeSuggestionAnywhere(
|
|
225
|
+
batchId: number,
|
|
226
|
+
index: number,
|
|
227
|
+
): { rt: SessionRuntime; text: string } | undefined {
|
|
228
|
+
this.ensureRestored();
|
|
229
|
+
for (const r of this.runtimes) {
|
|
230
|
+
const text = r.takeSuggestion(batchId, index);
|
|
231
|
+
if (text) return { rt: r, text };
|
|
232
|
+
}
|
|
233
|
+
return undefined;
|
|
234
|
+
}
|
|
235
|
+
|
|
131
236
|
/**
|
|
132
237
|
* Switch the chat to a project directory **without** waiting on ACP.
|
|
133
238
|
* - Reuses an existing controlled runtime for the same path when possible.
|
|
@@ -410,6 +515,9 @@ export class ChatController {
|
|
|
410
515
|
this.markSeen(rt);
|
|
411
516
|
this.persist();
|
|
412
517
|
};
|
|
518
|
+
rt.onTelegramMessageBound = (messageId, sessionId) => {
|
|
519
|
+
this.bindTelegramMessage(messageId, sessionId);
|
|
520
|
+
};
|
|
413
521
|
return rt;
|
|
414
522
|
}
|
|
415
523
|
|
|
@@ -466,3 +574,24 @@ export class ChatController {
|
|
|
466
574
|
function normPath(p: string): string {
|
|
467
575
|
return p.replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase();
|
|
468
576
|
}
|
|
577
|
+
|
|
578
|
+
/** Extract `#sess_xxxxxxxx` from bot message text/caption. */
|
|
579
|
+
function parseSessTag(text: string | undefined): string | undefined {
|
|
580
|
+
if (!text) return undefined;
|
|
581
|
+
const m = text.match(/#sess_([a-z0-9]{6,12})/i);
|
|
582
|
+
return m?.[1]?.toLowerCase();
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/** Match full session UUID (or short form) to a #sess_ tag body. */
|
|
586
|
+
function sessionMatchesTag(sessionId: string, tag: string): boolean {
|
|
587
|
+
const compact = sessionId.replace(/-/g, "").toLowerCase();
|
|
588
|
+
const short = sessionId.slice(0, 8).replace(/[^a-z0-9]/gi, "").toLowerCase();
|
|
589
|
+
const t = tag.toLowerCase();
|
|
590
|
+
return (
|
|
591
|
+
short === t ||
|
|
592
|
+
short.startsWith(t) ||
|
|
593
|
+
t.startsWith(short) ||
|
|
594
|
+
compact.startsWith(t) ||
|
|
595
|
+
sessionId.toLowerCase().startsWith(t)
|
|
596
|
+
);
|
|
597
|
+
}
|