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.
@@ -176,7 +176,8 @@ export class TelegramBotService {
176
176
  : `/${cmd}@${username}`;
177
177
 
178
178
  const extra: Record<string, unknown> = {};
179
- if (opts.messageThreadId !== undefined) {
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
 
@@ -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
- if (opts?.messageThreadId !== undefined) extra.message_thread_id = opts.messageThreadId;
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);
@@ -265,7 +265,8 @@ export class ForumManager {
265
265
  () =>
266
266
  this.api.sendMessage(
267
267
  this.groupId,
268
- `\u{1F916} **AI Chat** — general conversation (workspace).\nPath: \`${this.cfg.workspace}\``,
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",
@@ -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
- | { action: "send_prompt"; topic: string; prompt: string; newSession?: boolean }
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": "Topic title or #threadId", "prompt": "work for that topic", "new_session": false },',
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/AI Chat (GROK_WORKSPACE) you can create a project topic, set_path, then send_prompt with multi-step work.",
66
- " topic = exact topic title, #threadId, \"general\", or \"ai chat\". Optional new_session=true starts a fresh session there.",
67
- "- search_memory — search indexed forum topics + session titles/comments/history.",
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
- '```json',
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 the user-visible message. Prefer plain prose for the user; put protocol only in the fence.",
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 / AI Chat sessions use GROK_WORKSPACE; other topics use their bound project path.`,
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: always available against bot-owned session/topic indexes.");
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
- const topic = String(rec.topic ?? rec.name ?? rec.thread ?? rec.thread_id ?? rec.threadId ?? "").trim();
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, 4000),
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
  }
@@ -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