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/src/bot/telegram-bots.ts
CHANGED
|
@@ -176,7 +176,8 @@ export class TelegramBotService {
|
|
|
176
176
|
: `/${cmd}@${username}`;
|
|
177
177
|
|
|
178
178
|
const extra: Record<string, unknown> = {};
|
|
179
|
-
|
|
179
|
+
// Omit General (1) — Bot API rejects message_thread_id=1.
|
|
180
|
+
if (opts.messageThreadId !== undefined && opts.messageThreadId !== 1) {
|
|
180
181
|
extra.message_thread_id = opts.messageThreadId;
|
|
181
182
|
}
|
|
182
183
|
|
package/src/bot/telegram-io.ts
CHANGED
|
@@ -183,7 +183,10 @@ export async function sendMarkdownDoc(
|
|
|
183
183
|
opts?: { loud?: boolean; messageThreadId?: number },
|
|
184
184
|
): Promise<void> {
|
|
185
185
|
const extra: Record<string, unknown> = opts?.loud ? { disable_notification: false } : {};
|
|
186
|
-
|
|
186
|
+
// Omit General (1) — Bot API rejects message_thread_id=1.
|
|
187
|
+
if (opts?.messageThreadId !== undefined && opts.messageThreadId !== 1) {
|
|
188
|
+
extra.message_thread_id = opts.messageThreadId;
|
|
189
|
+
}
|
|
187
190
|
const rendered = toTelegramMarkdown(rawMarkdown);
|
|
188
191
|
const mdChunks = chunkMarkdown(rendered);
|
|
189
192
|
const plainChunks = chunkMarkdown(rawMarkdown);
|
package/src/forum/manager.ts
CHANGED
|
@@ -265,7 +265,8 @@ export class ForumManager {
|
|
|
265
265
|
() =>
|
|
266
266
|
this.api.sendMessage(
|
|
267
267
|
this.groupId,
|
|
268
|
-
`\u{1F916} **AI Chat** —
|
|
268
|
+
`\u{1F916} **AI Chat** — workspace coding chat.\nPath: \`${this.cfg.workspace}\`\n\n` +
|
|
269
|
+
`Use **General** to orchestrate projects (manager). Code here or in a project topic.`,
|
|
269
270
|
{ message_thread_id: topic.message_thread_id, parse_mode: "Markdown" },
|
|
270
271
|
),
|
|
271
272
|
"ai-chat-announce",
|
package/src/forum/thread.ts
CHANGED
|
@@ -14,3 +14,36 @@ export function batchKey(chatId: number, threadId: number | undefined, isForumGr
|
|
|
14
14
|
export function forumThreadId(threadId: number | undefined): number {
|
|
15
15
|
return threadId ?? FORUM_GENERAL_THREAD_ID;
|
|
16
16
|
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* True when this forum thread is the General manager topic.
|
|
20
|
+
* Only exact id `1` — do NOT treat private chats (undefined) as General.
|
|
21
|
+
* Callers should pass {@link forumThreadId} first when reading raw Telegram ids.
|
|
22
|
+
*/
|
|
23
|
+
export function isGeneralThread(threadId: number | undefined): boolean {
|
|
24
|
+
return threadId === FORUM_GENERAL_THREAD_ID;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Thread id for **outbound** Telegram API calls (`sendMessage`, `editMessage`, …).
|
|
29
|
+
*
|
|
30
|
+
* Critical: Bot API often rejects `message_thread_id: 1` for the General forum
|
|
31
|
+
* topic with "Bad Request: message thread not found". Omitting the field posts
|
|
32
|
+
* to General correctly. Real project topics (id > 1) must still pass the id.
|
|
33
|
+
*
|
|
34
|
+
* Inbound routing still uses {@link forumThreadId} (undefined → 1).
|
|
35
|
+
*/
|
|
36
|
+
export function outboundMessageThreadId(
|
|
37
|
+
threadId: number | undefined,
|
|
38
|
+
): number | undefined {
|
|
39
|
+
if (threadId === undefined || threadId === FORUM_GENERAL_THREAD_ID) return undefined;
|
|
40
|
+
return threadId;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Extra object for send/edit: only includes message_thread_id when safe. */
|
|
44
|
+
export function outboundThreadExtra(
|
|
45
|
+
threadId: number | undefined,
|
|
46
|
+
): { message_thread_id?: number } {
|
|
47
|
+
const id = outboundMessageThreadId(threadId);
|
|
48
|
+
return id !== undefined ? { message_thread_id: id } : {};
|
|
49
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Manager (OpenClaw-style) directive for the Telegram General topic.
|
|
3
|
+
*
|
|
4
|
+
* General is a chat-like orchestrator: it routes work to project topics,
|
|
5
|
+
* never implements project code itself, and reports statuses back to the user.
|
|
6
|
+
* Keep tidy-idempotent (no digit `{progress:…}` markers) so history cleaners
|
|
7
|
+
* can strip by exact match.
|
|
8
|
+
*/
|
|
9
|
+
import type { PromptInput } from "../app/types.js";
|
|
10
|
+
|
|
11
|
+
export const MANAGER_DIRECTIVE_MARKER = "MANAGER MODE (General topic — OpenClaw-style orchestrator):";
|
|
12
|
+
|
|
13
|
+
/** Marker for child-session completion wakes injected into General. */
|
|
14
|
+
export const MANAGER_WORK_REPORT_MARKER =
|
|
15
|
+
"MANAGER WORK REPORT (system — analyze and report to the user; do not invent facts):";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* First-prompt / steering block for General. Free of real progress digit tokens.
|
|
19
|
+
*/
|
|
20
|
+
export const MANAGER_DIRECTIVE = [
|
|
21
|
+
MANAGER_DIRECTIVE_MARKER,
|
|
22
|
+
"You are the global manager of this Telegram forum group — like OpenClaw.",
|
|
23
|
+
"This topic is a chat control room, not a coding workspace.",
|
|
24
|
+
"",
|
|
25
|
+
"How you behave:",
|
|
26
|
+
"- The user SEES your free-form prose in this chat. Always answer them in short chat text.",
|
|
27
|
+
"- Keep replies brief (a few sentences). No progress bars, tool dumps, job tables, or",
|
|
28
|
+
" \"Dispatching…\" / \"Sending to…\" narration.",
|
|
29
|
+
"- Use telegram JSON for side effects: search_memory, list_topics, send_prompt, notify.",
|
|
30
|
+
"- notify is an OPTIONAL extra ping — do NOT rely on it as the only user-facing channel.",
|
|
31
|
+
"- Example good reply: \"On it — continuing the ship gate in WindowsStoreListingGenerator.\"",
|
|
32
|
+
"- NEVER emit task-progress markers (no progress percent footers).",
|
|
33
|
+
"- NEVER enter plan mode, run self-recheck, or dump long tool/trace spam here.",
|
|
34
|
+
"- NEVER implement app code, edit project files, run builds/tests, or do multi-file work in General.",
|
|
35
|
+
"- Always DELEGATE real work to the correct project topic via telegram bridge actions.",
|
|
36
|
+
"",
|
|
37
|
+
"MEMORY-FIRST (mandatory — do this BEFORE any git/shell/file tools):",
|
|
38
|
+
"1. Read the auto-injected MANAGER CONTEXT (General history, memory hits, topics, jobs).",
|
|
39
|
+
"2. Call search_memory with the user's keywords (and list_topics if needed).",
|
|
40
|
+
"3. Prefer Telegram/bot memory + project topic session history over `git log` / filesystem.",
|
|
41
|
+
"4. Only use git if the user explicitly asks for git, or after memory has no useful hits.",
|
|
42
|
+
"5. Order of truth for \"what changed / last work\":",
|
|
43
|
+
" (a) General chat + manager memory hits with [age] stamps (newest first),",
|
|
44
|
+
" (b) that project's MOST RECENT topic sessions (highest recency / last user prompts / Done notes),",
|
|
45
|
+
" (c) ignore older sessions for the same app when a newer session exists,",
|
|
46
|
+
" (d) then optional git — never jump to git first.",
|
|
47
|
+
"6. When summarizing last work: weight last user prompts and assistant Done text by time,",
|
|
48
|
+
" not by how many times a keyword appears in an old session.",
|
|
49
|
+
"",
|
|
50
|
+
"Dispatch workflow:",
|
|
51
|
+
"1. Identify the target topic (exact title, #threadId, or create a new project topic).",
|
|
52
|
+
"2. Build a RICH child prompt from memory (what was done, what remains, acceptance criteria).",
|
|
53
|
+
"3. Emit one telegram JSON block: create_topic/set_path as needed, then send_prompt,",
|
|
54
|
+
" plus optional single notify if the user should hear about it.",
|
|
55
|
+
"4. After bridge results: silent is fine if dispatch ok; notify only on failure or if user asked.",
|
|
56
|
+
"5. MANAGER WORK REPORT wakes: notify only for outcomes the user needs (done/fail/important);",
|
|
57
|
+
" otherwise process silently (no notify).",
|
|
58
|
+
"",
|
|
59
|
+
"RESUME RELATED SESSIONS (critical):",
|
|
60
|
+
"- When memory/context shows a related session (session=019fc9ec or full UUID) and the user",
|
|
61
|
+
" wants a follow-up / continue / fix there, you MUST pass session_id on send_prompt.",
|
|
62
|
+
"- Without session_id the bridge uses the topic's CURRENT open session — often the wrong one.",
|
|
63
|
+
"- topic must be the EXACT forum title or #threadId from list_topics / memory — NEVER \"…\" / \"...\" / placeholders.",
|
|
64
|
+
"- If you only know the session id, omit topic: { \"action\": \"send_prompt\", \"session_id\": \"019fc9ec\", \"prompt\": \"...\" }",
|
|
65
|
+
"- Example: { \"action\": \"send_prompt\", \"topic\": \"MyApp\", \"session_id\": \"019fc9ec\", \"prompt\": \"...\" }",
|
|
66
|
+
"- Only omit session_id for brand-new work on the topic's foreground, or use new_session=true for a fresh session.",
|
|
67
|
+
"- On Topic not found: call list_topics and retry with the exact name or #id (or session_id only).",
|
|
68
|
+
"",
|
|
69
|
+
"New projects: create_topic with name + absolute path (folder is created if missing),",
|
|
70
|
+
"then send_prompt into that topic with the full kickoff instructions.",
|
|
71
|
+
"",
|
|
72
|
+
"User message:",
|
|
73
|
+
].join("\n");
|
|
74
|
+
|
|
75
|
+
/** True when text is a system work-report wake (meta; skip recheck / manager re-wrap noise). */
|
|
76
|
+
export function isManagerWorkReportPrompt(text: string): boolean {
|
|
77
|
+
return text.trimStart().startsWith(MANAGER_WORK_REPORT_MARKER);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Prepend manager directive (idempotent). */
|
|
81
|
+
export function wrapManagerDirective(input: PromptInput): PromptInput {
|
|
82
|
+
const body = input.text.trim() || "(see attached media / files)";
|
|
83
|
+
if (body.startsWith(MANAGER_DIRECTIVE_MARKER) || body.includes(MANAGER_DIRECTIVE_MARKER)) {
|
|
84
|
+
return input;
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
...input,
|
|
88
|
+
text: `${MANAGER_DIRECTIVE}\n${body}`,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Build the meta prompt that wakes General after a child topic finishes. */
|
|
93
|
+
export function buildManagerWorkReportPrompt(payload: {
|
|
94
|
+
jobId: string;
|
|
95
|
+
targetName: string;
|
|
96
|
+
targetThreadId: number;
|
|
97
|
+
targetPath: string;
|
|
98
|
+
userAskPreview: string;
|
|
99
|
+
dispatchPromptPreview: string;
|
|
100
|
+
status: "done" | "failed" | "cancelled";
|
|
101
|
+
stopReason?: string;
|
|
102
|
+
error?: string;
|
|
103
|
+
assistantSummary: string;
|
|
104
|
+
filesSummary?: string;
|
|
105
|
+
childSessionId?: string;
|
|
106
|
+
}): string {
|
|
107
|
+
const lines = [
|
|
108
|
+
MANAGER_WORK_REPORT_MARKER,
|
|
109
|
+
"```json",
|
|
110
|
+
JSON.stringify(
|
|
111
|
+
{
|
|
112
|
+
jobId: payload.jobId,
|
|
113
|
+
status: payload.status,
|
|
114
|
+
target: {
|
|
115
|
+
name: payload.targetName,
|
|
116
|
+
threadId: payload.targetThreadId,
|
|
117
|
+
path: payload.targetPath,
|
|
118
|
+
},
|
|
119
|
+
userAskPreview: payload.userAskPreview.slice(0, 500),
|
|
120
|
+
dispatchPromptPreview: payload.dispatchPromptPreview.slice(0, 800),
|
|
121
|
+
stopReason: payload.stopReason,
|
|
122
|
+
error: payload.error,
|
|
123
|
+
childSessionId: payload.childSessionId,
|
|
124
|
+
filesSummary: payload.filesSummary,
|
|
125
|
+
assistantSummary: payload.assistantSummary.slice(0, 3500),
|
|
126
|
+
},
|
|
127
|
+
null,
|
|
128
|
+
2,
|
|
129
|
+
),
|
|
130
|
+
"```",
|
|
131
|
+
"If the user needs to know (success, failure, blocked, needs a decision), reply in short prose.",
|
|
132
|
+
"If this is routine and they did not ask for a status, a one-line note is enough.",
|
|
133
|
+
"Do not re-emit the same send_prompt unless a retry is clearly useful.",
|
|
134
|
+
"No progress markers. No job tables or multi-message status spam.",
|
|
135
|
+
];
|
|
136
|
+
return lines.join("\n");
|
|
137
|
+
}
|
|
@@ -52,6 +52,14 @@ export function cleanCommentLine(raw: string, max = COMMENT_MAX): string {
|
|
|
52
52
|
*/
|
|
53
53
|
export function stripDirectiveWrappers(raw: string): string {
|
|
54
54
|
let t = raw.trim().replace(/^\([^)]*\)\s*/, "");
|
|
55
|
+
// Manager context + work reports (meta).
|
|
56
|
+
if (/^MANAGER WORK REPORT \(system/i.test(t)) t = "";
|
|
57
|
+
if (/MANAGER CONTEXT \(auto/i.test(t)) {
|
|
58
|
+
t = t.replace(/MANAGER CONTEXT \(auto[\s\S]*?\n---\n\n/i, "");
|
|
59
|
+
}
|
|
60
|
+
if (/^MANAGER MODE \(General topic/i.test(t)) {
|
|
61
|
+
t = t.replace(/^MANAGER MODE \(General topic[\s\S]*?User message:\s*/i, "");
|
|
62
|
+
}
|
|
55
63
|
const marker = "User's new message:";
|
|
56
64
|
const i = t.lastIndexOf(marker);
|
|
57
65
|
if (i !== -1) t = t.slice(i + marker.length);
|
|
@@ -68,6 +76,7 @@ export function stripDirectiveWrappers(raw: string): string {
|
|
|
68
76
|
}
|
|
69
77
|
}
|
|
70
78
|
if (/^TELEGRAM BRIDGE RESULTS \(system/i.test(t.trim())) t = "";
|
|
79
|
+
if (/^MANAGER WORK REPORT \(system/i.test(t.trim())) t = "";
|
|
71
80
|
return t.trim();
|
|
72
81
|
}
|
|
73
82
|
|
|
@@ -81,6 +90,7 @@ export function cleanUserPreview(raw: string, max = COMMENT_MAX): string {
|
|
|
81
90
|
if (/^SELF-RECHECK DECISION \(meta only\)/i.test(t)) return "";
|
|
82
91
|
if (/^FOLLOW-UP SUGGESTIONS \(meta only\)/i.test(t)) return "";
|
|
83
92
|
if (/^TELEGRAM BRIDGE RESULTS \(system/i.test(t)) return "Telegram bridge";
|
|
93
|
+
if (/^MANAGER WORK REPORT \(system/i.test(t)) return "Manager work report";
|
|
84
94
|
return cleanCommentLine(t, max);
|
|
85
95
|
}
|
|
86
96
|
|
|
@@ -25,11 +25,25 @@ export const TELEGRAM_SEND_PROMPT_MAX = 5;
|
|
|
25
25
|
export type TelegramAction =
|
|
26
26
|
| { action: "create_topic"; name: string; path?: string }
|
|
27
27
|
| { action: "set_path"; topic: string; path: string }
|
|
28
|
-
| {
|
|
28
|
+
| {
|
|
29
|
+
action: "send_prompt";
|
|
30
|
+
topic: string;
|
|
31
|
+
prompt: string;
|
|
32
|
+
newSession?: boolean;
|
|
33
|
+
/** Resume this exact Grok session in the topic (full id or short prefix). */
|
|
34
|
+
sessionId?: string;
|
|
35
|
+
}
|
|
36
|
+
/** User-facing message in the current chat (General: only way to talk to the user). */
|
|
37
|
+
| { action: "notify"; text: string; important?: boolean }
|
|
29
38
|
| { action: "search_memory"; query: string; limit?: number }
|
|
39
|
+
| { action: "list_topics" }
|
|
40
|
+
| { action: "list_jobs" }
|
|
30
41
|
| { action: "list_bots" }
|
|
31
42
|
| { action: "bot_command"; bot: string; command: string; args?: string };
|
|
32
43
|
|
|
44
|
+
/** Max user-facing notify messages per turn (anti-spam). */
|
|
45
|
+
export const TELEGRAM_NOTIFY_MAX = 3;
|
|
46
|
+
|
|
33
47
|
export interface TelegramActionExtract {
|
|
34
48
|
actions: TelegramAction[];
|
|
35
49
|
/** Text with telegram action fences removed. */
|
|
@@ -49,8 +63,11 @@ export const TELEGRAM_BRIDGE_DIRECTIVE_BASE = [
|
|
|
49
63
|
'{ "telegram": [',
|
|
50
64
|
' { "action": "create_topic", "name": "Topic title", "path": "optional absolute project path or exact catalog name" },',
|
|
51
65
|
' { "action": "set_path", "topic": "Topic title or #threadId", "path": "absolute path or exact catalog name" },',
|
|
52
|
-
' { "action": "send_prompt", "topic": "
|
|
66
|
+
' { "action": "send_prompt", "topic": "ExactTopicTitle or #threadId", "prompt": "work for that topic", "new_session": false, "session_id": "optional-full-or-short-session-id" },',
|
|
67
|
+
' { "action": "notify", "text": "short message for the user", "important": false },',
|
|
53
68
|
' { "action": "search_memory", "query": "keywords about past work", "limit": 8 },',
|
|
69
|
+
' { "action": "list_topics" },',
|
|
70
|
+
' { "action": "list_jobs" },',
|
|
54
71
|
' { "action": "list_bots" },',
|
|
55
72
|
' { "action": "bot_command", "bot": "username_without_at", "command": "status", "args": "optional" }',
|
|
56
73
|
"] }",
|
|
@@ -62,22 +79,50 @@ export const TELEGRAM_BRIDGE_DIRECTIVE_BASE = [
|
|
|
62
79
|
"- set_path — bind/rebind an existing topic to a project path (absolute dir or exact catalog name).",
|
|
63
80
|
" Absolute paths that do not exist yet are created on disk.",
|
|
64
81
|
"- send_prompt — start or queue a prompt in another topic's session (does not wait for that turn to finish).",
|
|
65
|
-
" From General
|
|
66
|
-
"
|
|
67
|
-
"
|
|
82
|
+
" From General (manager) you MUST create/set path then send_prompt — never code in General.",
|
|
83
|
+
" Child sessions started from General auto report back with a MANAGER WORK REPORT when done.",
|
|
84
|
+
" topic = EXACT forum title (from list_topics / memory), #threadId, \"general\", or \"ai chat\".",
|
|
85
|
+
" NEVER use placeholders like \"…\", \"...\", \"topic\", \"there\" — call list_topics if unsure.",
|
|
86
|
+
" Optional new_session=true starts a FRESH session in that topic.",
|
|
87
|
+
" Optional session_id (full UUID or short prefix like 019fc9ec) RESUMES that exact session.",
|
|
88
|
+
" When session_id is set, topic may be omitted — the bridge maps session path → topic.",
|
|
89
|
+
" session_id is REQUIRED when memory found a related session and the user wants a follow-up there.",
|
|
90
|
+
" Without session_id the bridge uses the topic's current foreground session (often wrong).",
|
|
91
|
+
"- notify — optional extra short message. In General the user ALSO sees your free-form prose,",
|
|
92
|
+
" so always write a brief chat reply; do not stay silent after search/dispatch.",
|
|
93
|
+
" Max 3 notify per turn. Never notify dispatch chatter, job tables, or \"sending to…\" spam.",
|
|
94
|
+
"- search_memory — search indexed forum topics + session titles/comments/history (memory-first).",
|
|
95
|
+
"- list_topics — list all mapped forum topics (name, #id, path, kind).",
|
|
96
|
+
"- list_jobs — list recent manager dispatches from General (status, target topic).",
|
|
68
97
|
"- list_bots — list allowlisted sibling Telegram bots and command catalogs.",
|
|
69
98
|
"- bot_command — /command@bot; waits for that bot to settle. NOT a Done; timeouts return ok=false.",
|
|
70
99
|
"",
|
|
100
|
+
"Example (General — short prose plus dispatch):",
|
|
101
|
+
"```json",
|
|
102
|
+
'{ "telegram": [',
|
|
103
|
+
' { "action": "send_prompt", "topic": "WindowsStoreListingGenerator", "session_id": "019fc9ec", "prompt": "Continue ship gate." },',
|
|
104
|
+
' { "action": "notify", "text": "On it — continuing the ship gate in WindowsStoreListingGenerator." }',
|
|
105
|
+
"] }",
|
|
106
|
+
"```",
|
|
107
|
+
"",
|
|
108
|
+
"Example (session_id only — topic inferred from session path):",
|
|
109
|
+
"```json",
|
|
110
|
+
'{ "telegram": [',
|
|
111
|
+
' { "action": "send_prompt", "session_id": "019fc9ec", "prompt": "Continue the related work." }',
|
|
112
|
+
"] }",
|
|
113
|
+
"```",
|
|
114
|
+
"",
|
|
71
115
|
"Example (from General): create project topic + path + kick off work there:",
|
|
72
|
-
|
|
116
|
+
"```json",
|
|
73
117
|
'{ "telegram": [',
|
|
74
118
|
' { "action": "create_topic", "name": "MyApp", "path": "H:\\\\Projects\\\\MyApp" },',
|
|
75
|
-
' { "action": "send_prompt", "topic": "MyApp", "prompt": "1) scaffold\\n2) tests\\n3) README" }',
|
|
119
|
+
' { "action": "send_prompt", "topic": "MyApp", "new_session": true, "prompt": "1) scaffold\\n2) tests\\n3) README" },',
|
|
120
|
+
' { "action": "notify", "text": "Created **MyApp** and started scaffolding there." }',
|
|
76
121
|
"] }",
|
|
77
122
|
"```",
|
|
78
123
|
"",
|
|
79
124
|
"After you emit these actions, the bridge runs them and may send TELEGRAM BRIDGE RESULTS. Use those facts; do not invent replies.",
|
|
80
|
-
"The bridge strips the JSON fence from
|
|
125
|
+
"The bridge strips the JSON fence from display. In General write a short prose answer the user can see.",
|
|
81
126
|
"Do not spam actions. Prefer one orchestration block per turn.",
|
|
82
127
|
].join("\n");
|
|
83
128
|
|
|
@@ -87,6 +132,8 @@ export interface TelegramBridgeCaps {
|
|
|
87
132
|
allowedBots: string[];
|
|
88
133
|
/** username → command list for first-prompt teaching */
|
|
89
134
|
botCommands?: Record<string, Array<{ command: string; description?: string }>>;
|
|
135
|
+
/** When true, General manager wording is appended. */
|
|
136
|
+
managerMode?: boolean;
|
|
90
137
|
}
|
|
91
138
|
|
|
92
139
|
/** Full first-prompt directive including live capabilities. */
|
|
@@ -95,7 +142,7 @@ export function buildTelegramBridgeDirective(caps: TelegramBridgeCaps): string {
|
|
|
95
142
|
if (caps.forumReady && caps.topicGroupId !== undefined) {
|
|
96
143
|
lines.push(
|
|
97
144
|
`- Forum topics: READY (group ${caps.topicGroupId}). You may create_topic, set_path, and send_prompt.`,
|
|
98
|
-
`- General
|
|
145
|
+
`- General is the manager topic (orchestrates only). AI Chat uses GROK_WORKSPACE for coding there; project topics use their bound path.`,
|
|
99
146
|
);
|
|
100
147
|
} else if (caps.topicGroupId !== undefined) {
|
|
101
148
|
lines.push(
|
|
@@ -123,7 +170,12 @@ export function buildTelegramBridgeDirective(caps: TelegramBridgeCaps): string {
|
|
|
123
170
|
"- Sibling bots: none configured (ALLOWED_TELEGRAM_BOTS empty). list_bots returns empty; bot_command disabled.",
|
|
124
171
|
);
|
|
125
172
|
}
|
|
126
|
-
lines.push("- search_memory:
|
|
173
|
+
lines.push("- search_memory / list_topics / list_jobs: available against bot-owned indexes.");
|
|
174
|
+
if (caps.managerMode) {
|
|
175
|
+
lines.push(
|
|
176
|
+
"- You are in General manager mode: chat-like replies only; memory-first; dispatch via send_prompt; child work auto-reports back.",
|
|
177
|
+
);
|
|
178
|
+
}
|
|
127
179
|
return lines.join("\n");
|
|
128
180
|
}
|
|
129
181
|
|
|
@@ -165,6 +217,7 @@ export function buildTelegramBridgeResultsPrompt(results: unknown[]): string {
|
|
|
165
217
|
payload,
|
|
166
218
|
"```",
|
|
167
219
|
"Continue the user's task with this information. Do not ask the user to paste results. Do not re-emit the same telegram actions unless something failed and a retry is useful.",
|
|
220
|
+
"If actions succeeded, continue with a short prose answer — never dump job tables or dispatch narration.",
|
|
168
221
|
].join("\n");
|
|
169
222
|
}
|
|
170
223
|
|
|
@@ -190,7 +243,7 @@ export function extractTelegramActions(text: string): TelegramActionExtract {
|
|
|
190
243
|
cleaned = cleaned.replace(/```(?:json|JSON)?\s*\r?\n[\s\S]*$/i, (tail) => {
|
|
191
244
|
if (
|
|
192
245
|
/"telegram"\s*:/i.test(tail) ||
|
|
193
|
-
/"action"\s*:\s*"(?:create_topic|set_path|send_prompt|search_memory|list_bots|bot_command)"/i.test(
|
|
246
|
+
/"action"\s*:\s*"(?:create_topic|set_path|send_prompt|notify|search_memory|list_topics|list_jobs|list_bots|bot_command)"/i.test(
|
|
194
247
|
tail,
|
|
195
248
|
)
|
|
196
249
|
) {
|
|
@@ -280,19 +333,58 @@ function normalizeAction(item: unknown): TelegramAction | undefined {
|
|
|
280
333
|
case "send_prompt":
|
|
281
334
|
case "topic_prompt":
|
|
282
335
|
case "prompt_topic": {
|
|
283
|
-
|
|
336
|
+
let topic = String(rec.topic ?? rec.name ?? rec.thread ?? rec.thread_id ?? rec.threadId ?? "").trim();
|
|
284
337
|
const prompt = String(rec.prompt ?? rec.text ?? rec.message ?? "").trim();
|
|
285
|
-
if (!topic || !prompt) return undefined;
|
|
286
338
|
const newSessionRaw = rec.new_session ?? rec.newSession ?? rec.fresh;
|
|
287
339
|
const newSession =
|
|
288
340
|
newSessionRaw === true ||
|
|
289
341
|
newSessionRaw === 1 ||
|
|
290
342
|
/^(1|true|yes|y)$/i.test(String(newSessionRaw ?? "").trim());
|
|
343
|
+
const sessionIdRaw = String(
|
|
344
|
+
rec.session_id ?? rec.sessionId ?? rec.sess ?? rec.session ?? "",
|
|
345
|
+
).trim();
|
|
346
|
+
const sessionId = sessionIdRaw
|
|
347
|
+
? sessionIdRaw.replace(/^#?sess[_-]?/i, "").slice(0, 64)
|
|
348
|
+
: undefined;
|
|
349
|
+
// Drop placeholder topics ("…") so resolve can use session_id / memory inference.
|
|
350
|
+
if (
|
|
351
|
+
!topic ||
|
|
352
|
+
/^[\s.…·•⋯︙\-–—_*~`'"“”‘’\u2026\u22ef\u3002]+$/u.test(topic) ||
|
|
353
|
+
/^(topic|there|here|same|related|todo|tbd|none|null|target)$/i.test(topic)
|
|
354
|
+
) {
|
|
355
|
+
topic = "";
|
|
356
|
+
}
|
|
357
|
+
// topic optional when session_id is present OR prompt alone (memory may infer topic).
|
|
358
|
+
if (!prompt) return undefined;
|
|
359
|
+
// Allow prompt-only send_prompt: bridge may infer topic from memory + user ask.
|
|
360
|
+
// Still require at least something actionable (prompt is enough).
|
|
361
|
+
// Full prompt for the target agent — do NOT hard-crop to Telegram size.
|
|
362
|
+
// Long prompts are split only in the human-visible announce message.
|
|
363
|
+
const PROMPT_MAX = 100_000;
|
|
291
364
|
return {
|
|
292
365
|
action: "send_prompt",
|
|
293
366
|
topic: topic.slice(0, 128),
|
|
294
|
-
prompt: prompt.slice(0,
|
|
367
|
+
prompt: prompt.length > PROMPT_MAX ? prompt.slice(0, PROMPT_MAX) : prompt,
|
|
295
368
|
newSession: newSession || undefined,
|
|
369
|
+
// session_id wins over new_session when both are set (resume is explicit).
|
|
370
|
+
sessionId: sessionId || undefined,
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
case "notify":
|
|
374
|
+
case "message_user":
|
|
375
|
+
case "say":
|
|
376
|
+
case "tell_user": {
|
|
377
|
+
const text = String(rec.text ?? rec.message ?? rec.body ?? rec.prompt ?? "").trim();
|
|
378
|
+
if (!text) return undefined;
|
|
379
|
+
const impRaw = rec.important ?? rec.priority ?? rec.loud;
|
|
380
|
+
const important =
|
|
381
|
+
impRaw === true ||
|
|
382
|
+
impRaw === 1 ||
|
|
383
|
+
/^(1|true|yes|y|high|urgent|important)$/i.test(String(impRaw ?? "").trim());
|
|
384
|
+
return {
|
|
385
|
+
action: "notify",
|
|
386
|
+
text: text.slice(0, 3500),
|
|
387
|
+
important: important || undefined,
|
|
296
388
|
};
|
|
297
389
|
}
|
|
298
390
|
case "search_memory":
|
|
@@ -305,6 +397,13 @@ function normalizeAction(item: unknown): TelegramAction | undefined {
|
|
|
305
397
|
limit = Math.max(1, Math.min(20, Math.round(limit)));
|
|
306
398
|
return { action: "search_memory", query: query.slice(0, 300), limit };
|
|
307
399
|
}
|
|
400
|
+
case "list_topics":
|
|
401
|
+
case "topics":
|
|
402
|
+
return { action: "list_topics" };
|
|
403
|
+
case "list_jobs":
|
|
404
|
+
case "jobs":
|
|
405
|
+
case "manager_jobs":
|
|
406
|
+
return { action: "list_jobs" };
|
|
308
407
|
case "list_bots":
|
|
309
408
|
case "bots":
|
|
310
409
|
return { action: "list_bots" };
|
|
@@ -332,6 +431,7 @@ function normalizeAction(item: unknown): TelegramAction | undefined {
|
|
|
332
431
|
function capBotCommands(actions: TelegramAction[]): TelegramAction[] {
|
|
333
432
|
let botCmds = 0;
|
|
334
433
|
let sendPrompts = 0;
|
|
434
|
+
let notifies = 0;
|
|
335
435
|
const out: TelegramAction[] = [];
|
|
336
436
|
for (const a of actions) {
|
|
337
437
|
if (a.action === "bot_command") {
|
|
@@ -342,6 +442,10 @@ function capBotCommands(actions: TelegramAction[]): TelegramAction[] {
|
|
|
342
442
|
if (sendPrompts >= TELEGRAM_SEND_PROMPT_MAX) continue;
|
|
343
443
|
sendPrompts++;
|
|
344
444
|
}
|
|
445
|
+
if (a.action === "notify") {
|
|
446
|
+
if (notifies >= TELEGRAM_NOTIFY_MAX) continue;
|
|
447
|
+
notifies++;
|
|
448
|
+
}
|
|
345
449
|
out.push(a);
|
|
346
450
|
if (out.length >= TELEGRAM_ACTION_MAX) break;
|
|
347
451
|
}
|
package/src/sessions/history.ts
CHANGED
|
@@ -10,8 +10,15 @@ import {
|
|
|
10
10
|
TELEGRAM_BRIDGE_MARKER,
|
|
11
11
|
TELEGRAM_BRIDGE_RESULTS_MARKER,
|
|
12
12
|
} from "../render/telegram-bridge.js";
|
|
13
|
+
import {
|
|
14
|
+
MANAGER_DIRECTIVE_MARKER,
|
|
15
|
+
MANAGER_WORK_REPORT_MARKER,
|
|
16
|
+
} from "../render/manager-directive.js";
|
|
13
17
|
import type { HistoryEntry, HistoryRole } from "./types.js";
|
|
14
18
|
|
|
19
|
+
/** Keep in sync with bot/manager-context.ts (avoid sessions→bot import). */
|
|
20
|
+
const MANAGER_CONTEXT_MARKER = "MANAGER CONTEXT (auto — use before dispatching work):";
|
|
21
|
+
|
|
15
22
|
const TAIL_WINDOWS = [256 * 1024, 1024 * 1024, 4 * 1024 * 1024]; // grow until entries found
|
|
16
23
|
|
|
17
24
|
interface RawEvent {
|
|
@@ -261,6 +268,17 @@ function cleanStoredText(text: string): string {
|
|
|
261
268
|
if (/^SELF-RECHECK DECISION \(meta only\)/i.test(t.trim())) t = "";
|
|
262
269
|
if (/^SELF-RECHECK \(automatic quality pass/i.test(t.trim())) t = "";
|
|
263
270
|
if (t.trimStart().startsWith(TELEGRAM_BRIDGE_RESULTS_MARKER)) t = "";
|
|
271
|
+
if (t.trimStart().startsWith(MANAGER_WORK_REPORT_MARKER)) t = "";
|
|
272
|
+
// Manager directive / context: keep only the user message tail.
|
|
273
|
+
if (t.includes(MANAGER_CONTEXT_MARKER)) {
|
|
274
|
+
const split = t.split(/\n---\n\n/);
|
|
275
|
+
if (split.length > 1) t = split.slice(1).join("\n---\n\n").trim();
|
|
276
|
+
}
|
|
277
|
+
if (t.includes(MANAGER_DIRECTIVE_MARKER)) {
|
|
278
|
+
const um = "User message:";
|
|
279
|
+
const ui = t.lastIndexOf(um);
|
|
280
|
+
if (ui !== -1) t = t.slice(ui + um.length).trim();
|
|
281
|
+
}
|
|
264
282
|
return t;
|
|
265
283
|
}
|
|
266
284
|
|