grok-telegram-bot 2.5.0 → 2.7.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.
Files changed (50) hide show
  1. package/.env.example +13 -0
  2. package/CHANGELOG.md +106 -0
  3. package/README.md +20 -5
  4. package/docs/GROUP.md +39 -4
  5. package/docs/INSTALL.md +2 -0
  6. package/package.json +4 -4
  7. package/scripts/setup.mjs +20 -3
  8. package/src/app/instance.ts +223 -0
  9. package/src/app/types.ts +34 -1
  10. package/src/bot/ask-user-service.ts +226 -0
  11. package/src/bot/auth.ts +5 -1
  12. package/src/bot/bot.ts +105 -4
  13. package/src/bot/chat-controller.ts +129 -0
  14. package/src/bot/commands.ts +22 -2
  15. package/src/bot/group-memory.ts +192 -12
  16. package/src/bot/handlers/forum.ts +16 -6
  17. package/src/bot/handlers/grok-slash.ts +336 -0
  18. package/src/bot/handlers/message.ts +165 -25
  19. package/src/bot/handlers/photo.ts +4 -1
  20. package/src/bot/handlers/system.ts +63 -1
  21. package/src/bot/image-return.ts +4 -1
  22. package/src/bot/manager-context.ts +208 -0
  23. package/src/bot/manager-jobs.ts +142 -0
  24. package/src/bot/menu/ephemeral.ts +4 -1
  25. package/src/bot/plan-exit-service.ts +169 -0
  26. package/src/bot/prompt-anchor.ts +2 -3
  27. package/src/bot/prompt-content.ts +5 -0
  28. package/src/bot/registry.ts +11 -2
  29. package/src/bot/scope.ts +9 -8
  30. package/src/bot/session-runtime.ts +665 -55
  31. package/src/bot/telegram-actions.ts +728 -38
  32. package/src/bot/telegram-bots.ts +2 -1
  33. package/src/bot/telegram-io.ts +4 -1
  34. package/src/cli.ts +43 -7
  35. package/src/config.ts +35 -25
  36. package/src/forum/manager.ts +2 -1
  37. package/src/forum/thread.ts +33 -0
  38. package/src/grok/client.ts +29 -5
  39. package/src/grok/plan-approval.ts +8 -0
  40. package/src/index.ts +4 -0
  41. package/src/render/manager-directive.ts +137 -0
  42. package/src/render/session-comment.ts +10 -0
  43. package/src/render/telegram-bridge.ts +118 -14
  44. package/src/service/linux.ts +21 -15
  45. package/src/service/macos.ts +20 -15
  46. package/src/service/platform.ts +12 -3
  47. package/src/service/types.ts +6 -0
  48. package/src/service/windows.ts +31 -22
  49. package/src/sessions/history.ts +18 -0
  50. package/src/stream/streamer.ts +46 -10
@@ -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);
package/src/cli.ts CHANGED
@@ -10,16 +10,17 @@
10
10
  import { spawnSync } from "node:child_process";
11
11
  import { existsSync, readFileSync } from "node:fs";
12
12
  import { join } from "node:path";
13
+ import { listKnownInstances, stripInstanceFlags } from "./app/instance.js";
13
14
  import { ENV_PATH, INSTANCE_DIR, PROJECT_ROOT } from "./config.js";
14
15
  import { buildLaunchSpec, getController } from "./service/index.js";
15
16
 
16
17
  const HELP = `Grok Telegram Bot — CLI
17
18
 
18
- Usage: grok-tg <command>
19
+ Usage: grok-tg [--name <slug>] [--instance <dir>] <command>
19
20
 
20
21
  run Run in the foreground
21
- setup [--path] Create/update .env (default ~/.grok/tg/.env, loaded from
22
- any folder); --path just prints the resolved .env location
22
+ setup [--path] Create/update .env (default ~/.grok/tg/.env);
23
+ --name <slug> writes ~/.grok/tg/instances/<slug>/.env
23
24
  install Install + start a background service (autostart on boot)
24
25
  uninstall Stop + remove the background service
25
26
  start Start the service
@@ -27,12 +28,20 @@ Usage: grok-tg <command>
27
28
  restart Restart the service
28
29
  status Show install + running status
29
30
  logs [n] Show the last n log lines (default 100)
31
+ instances List named bot instances on this host
30
32
  help Show this help
33
+
34
+ Several Telegram bots on one host (one chat per project):
35
+
36
+ grok-tg --name work setup <BOT_TOKEN> <YOUR_USER_ID>
37
+ grok-tg --name work install
38
+ grok-tg --name work status
31
39
  `;
32
40
 
33
41
  async function main(): Promise<void> {
34
42
  const args = process.argv.slice(2);
35
- const [cmd, arg] = args;
43
+ const rest = stripInstanceFlags(args);
44
+ const [cmd, arg] = rest;
36
45
 
37
46
  switch (cmd) {
38
47
  case "run":
@@ -45,9 +54,9 @@ async function main(): Promise<void> {
45
54
  // Run the plain-node setup script, targeting this folder (.env lives in
46
55
  // the instance dir). Pass through optional <token> [userId] args.
47
56
  const script = join(PROJECT_ROOT, "scripts", "setup.mjs");
48
- const r = spawnSync(process.execPath, [script, ...args.slice(1)], {
57
+ const r = spawnSync(process.execPath, [script, "--instance", INSTANCE_DIR, ...rest.slice(1)], {
49
58
  stdio: "inherit",
50
- env: { ...process.env, GROK_TG_CWD: INSTANCE_DIR },
59
+ env: { ...process.env, GROK_TG_DIR: INSTANCE_DIR, GROK_TG_CWD: INSTANCE_DIR },
51
60
  });
52
61
  process.exit(r.status ?? 0);
53
62
  break;
@@ -57,7 +66,11 @@ async function main(): Promise<void> {
57
66
  preflight();
58
67
  const r = await getController().install(buildLaunchSpec());
59
68
  console.log(r.ok ? `✓ ${r.message}` : `✗ ${r.message}`);
60
- if (r.ok) console.log("\nManage it with: grok-tg status | stop | restart | logs");
69
+ if (r.ok) {
70
+ const spec = buildLaunchSpec();
71
+ const flag = spec.slug ? `--name ${spec.slug} ` : "";
72
+ console.log(`\nManage it with: grok-tg ${flag}status | stop | restart | logs`);
73
+ }
61
74
  process.exit(r.ok ? 0 : 1);
62
75
  break;
63
76
  }
@@ -86,6 +99,11 @@ async function main(): Promise<void> {
86
99
  process.exit(0);
87
100
  break;
88
101
 
102
+ case "instances":
103
+ printInstances();
104
+ process.exit(0);
105
+ break;
106
+
89
107
  case "help":
90
108
  case "--help":
91
109
  case "-h":
@@ -112,6 +130,24 @@ function preflight(): void {
112
130
  }
113
131
  }
114
132
 
133
+ function printInstances(): void {
134
+ const items = listKnownInstances();
135
+ if (items.length === 0) {
136
+ console.log("No instances found.");
137
+ console.log(" grok-tg setup # default bot (~/.grok/tg)");
138
+ console.log(" grok-tg --name work setup <token> <userId> # second bot");
139
+ return;
140
+ }
141
+ console.log("Instances:\n");
142
+ for (const it of items) {
143
+ const manage = it.slug ? `grok-tg --name ${it.slug}` : "grok-tg";
144
+ console.log(` ${it.name}`);
145
+ console.log(` dir: ${it.dir}`);
146
+ console.log(` service: ${it.identity.id}`);
147
+ console.log(` manage: ${manage} status | restart | logs\n`);
148
+ }
149
+ }
150
+
115
151
  function printLogs(n: number): void {
116
152
  const file = buildLaunchSpec().logFile;
117
153
  if (!existsSync(file)) {
package/src/config.ts CHANGED
@@ -10,51 +10,42 @@ import { existsSync } from "node:fs";
10
10
  import { homedir } from "node:os";
11
11
  import { dirname, isAbsolute, join, resolve } from "node:path";
12
12
  import { fileURLToPath } from "node:url";
13
+ import {
14
+ CANONICAL_DIR,
15
+ expandHome,
16
+ resolveInstanceDir as resolveNamedInstanceDir,
17
+ } from "./app/instance.js";
18
+
19
+ export { CANONICAL_DIR, expandHome };
13
20
 
14
21
  /** Absolute path to the installed bot code (one level above src/). For a global
15
22
  * npm install this lives inside node_modules — code lives here, never user data. */
16
23
  export const PROJECT_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
17
24
 
18
- /** Canonical, path-independent home for this bot's `.env`, `logs/`, `data/` and
19
- * the single-instance locks: `~/.grok/tg`. Used whenever the bot is started
20
- * without an explicit instance dir and there's no `.env` in the current folder,
21
- * so the SAME configuration is found no matter which directory you launch from. */
22
- export const CANONICAL_DIR = join(homedir(), ".grok", "tg");
23
-
24
25
  /**
25
26
  * Directory holding THIS instance's `.env`, `logs/` and `data/`. Resolution
26
27
  * (first match wins):
27
28
  * 1. `--instance <dir>` argv — set by the installed background service,
28
- * 2. `GROK_TG_DIR` env — an explicit override,
29
- * 3. `GROK_TG_CWD` env — the legacy launcher variable,
30
- * 4. the current folder, IF it already contains a `.env`,
29
+ * 2. `--name` / `GROK_TG_NAME` — named instance under `~/.grok/tg/instances/`,
30
+ * 3. `GROK_TG_DIR` env — an explicit override,
31
+ * 4. `GROK_TG_CWD` or cwd IF that folder already contains a `.env`,
31
32
  * 5. the canonical `~/.grok/tg` home — the path-independent default.
32
33
  */
33
- export const INSTANCE_DIR = resolveInstanceDir();
34
+ export const INSTANCE_DIR = resolveNamedInstanceDir({
35
+ argv: process.argv,
36
+ envDir: process.env.GROK_TG_DIR,
37
+ nameEnv: process.env.GROK_TG_NAME,
38
+ cwdHint: process.env.GROK_TG_CWD,
39
+ });
34
40
 
35
41
  /** Absolute path to the `.env` this instance loads (and that `setup` writes). */
36
42
  export const ENV_PATH = join(INSTANCE_DIR, ".env");
37
43
 
38
- function resolveInstanceDir(): string {
39
- const flag = process.argv.indexOf("--instance");
40
- if (flag !== -1 && process.argv[flag + 1]) return resolve(process.argv[flag + 1]!);
41
- const envDir = process.env.GROK_TG_DIR?.trim() || process.env.GROK_TG_CWD?.trim();
42
- if (envDir) return resolve(expandHome(envDir));
43
- if (existsSync(join(process.cwd(), ".env"))) return process.cwd();
44
- return CANONICAL_DIR;
45
- }
46
-
47
44
  // Load .env from the resolved instance directory. Keep the parsed values as
48
45
  // well: a machine-wide TELEGRAM_BOT_TOKEN may belong to a sibling bot (Codex,
49
46
  // Kiro, etc.) and must never override this Grok instance's identity.
50
47
  const instanceEnv = loadDotenv({ path: ENV_PATH }).parsed ?? {};
51
48
 
52
- function expandHome(p: string): string {
53
- if (p === "~") return homedir();
54
- if (p.startsWith("~/") || p.startsWith("~\\")) return join(homedir(), p.slice(2));
55
- return p;
56
- }
57
-
58
49
  function bool(v: string | undefined, def: boolean): boolean {
59
50
  if (v === undefined || v === "") return def;
60
51
  return ["1", "true", "yes", "on"].includes(v.toLowerCase());
@@ -142,6 +133,20 @@ export interface AppConfig {
142
133
  * Approve/Deny buttons.
143
134
  */
144
135
  autoApprovePermissions: boolean;
136
+ /**
137
+ * Auto-approve Grok plan-mode exit (no Approve/Changes/Abandon buttons).
138
+ * Default true so unattended/24/7 bots never wait on a TUI. Set false for
139
+ * interactive review in Telegram.
140
+ */
141
+ autoApprovePlan: boolean;
142
+ /** GROK_SANDBOX profile (workspace-safe, strict, off, …). */
143
+ sandboxProfile?: string;
144
+ /** GROK_MEMORY setting forwarded to the agent process. */
145
+ grokMemory?: string;
146
+ /** `--agent-profile` for `grok agent`. */
147
+ agentProfile?: string;
148
+ /** `--plugin-dir` for `grok agent`. */
149
+ pluginDir?: string;
145
150
  projectRoots: string[];
146
151
  streamThrottleMs: number;
147
152
  messageBatchMs: number;
@@ -301,6 +306,11 @@ export function loadConfig(): AppConfig {
301
306
  trustAllTools: bool(process.env.GROK_TRUST_ALL_TOOLS, true),
302
307
  // Default true: auto-approve with session-scope when the agent still asks.
303
308
  autoApprovePermissions: bool(process.env.AUTO_APPROVE_PERMISSIONS, true),
309
+ autoApprovePlan: bool(process.env.AUTO_APPROVE_PLAN, true),
310
+ sandboxProfile: process.env.GROK_SANDBOX?.trim() || undefined,
311
+ grokMemory: process.env.GROK_MEMORY?.trim() || undefined,
312
+ agentProfile: process.env.GROK_AGENT_PROFILE?.trim() || undefined,
313
+ pluginDir: process.env.GROK_PLUGIN_DIR?.trim() || undefined,
304
314
  projectRoots: [...new Set(roots)],
305
315
  streamThrottleMs: num(process.env.STREAM_THROTTLE_MS, 1500),
306
316
  messageBatchMs: nonNegNum(process.env.MESSAGE_BATCH_MS, 800),
@@ -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
+ }
@@ -218,6 +218,10 @@ export interface GrokClientOptions {
218
218
  autoRestart?: boolean;
219
219
  promptIdleTimeoutMs?: number;
220
220
  promptMaxMs?: number;
221
+ sandboxProfile?: string;
222
+ grokMemory?: string;
223
+ agentProfile?: string;
224
+ pluginDir?: string;
221
225
  }
222
226
 
223
227
  interface Pending {
@@ -288,6 +292,10 @@ export class GrokClient extends EventEmitter {
288
292
  * outcomes). Must never kill the agent process.
289
293
  */
290
294
  onSessionCancel?: (sessionId: string) => void;
295
+ /** Interactive (or auto) plan-mode exit. Default: auto-approve. */
296
+ planExitHandler?: (params: Record<string, unknown>) => Promise<unknown>;
297
+ /** Interactive (or skip) ask_user_question. Default: SkipInterview. */
298
+ askUserHandler?: (params: Record<string, unknown>) => Promise<unknown>;
291
299
 
292
300
  constructor(private readonly opts: GrokClientOptions) {
293
301
  super();
@@ -313,11 +321,15 @@ export class GrokClient extends EventEmitter {
313
321
  // the new token. `--no-auto-update` was removed in grok 0.2.x (exit 2).
314
322
  const args = ["agent", "--no-leader"];
315
323
  if (this.opts.trustAllTools) args.push("--always-approve");
324
+ if (this.opts.agentProfile) args.push("--agent-profile", this.opts.agentProfile);
325
+ if (this.opts.pluginDir) args.push("--plugin-dir", this.opts.pluginDir);
316
326
  args.push("stdio");
317
327
 
318
328
  log.info(`spawning: ${this.opts.grokCliPath} ${args.join(" ")}`);
319
329
  const env = { ...process.env };
320
330
  if (this.opts.apiKey) env.XAI_API_KEY = this.opts.apiKey;
331
+ if (this.opts.sandboxProfile) env.GROK_SANDBOX = this.opts.sandboxProfile;
332
+ if (this.opts.grokMemory) env.GROK_MEMORY = this.opts.grokMemory;
321
333
  const proc = spawn(this.opts.grokCliPath, args, {
322
334
  stdio: ["pipe", "pipe", "pipe"],
323
335
  cwd: this.opts.workspace,
@@ -419,7 +431,11 @@ export class GrokClient extends EventEmitter {
419
431
  }
420
432
 
421
433
  async newSession(cwd: string): Promise<string> {
422
- const res = (await this.request("session/new", { cwd, mcpServers: [] })) as { sessionId: string };
434
+ const res = (await this.request("session/new", {
435
+ cwd,
436
+ mcpServers: [],
437
+ ...(this.opts.trustAllTools ? { _meta: { yoloMode: true } } : {}),
438
+ })) as { sessionId: string };
423
439
  this.parseSessionExtras(res);
424
440
  this.cwd.set(res.sessionId, cwd);
425
441
  this.slog.create(res.sessionId, cwd);
@@ -622,6 +638,12 @@ export class GrokClient extends EventEmitter {
622
638
  return this.request("_grok.dev/commands/execute", { sessionId, command });
623
639
  }
624
640
 
641
+ /** Update spawn-time agent env (applied on the next `grok agent` restart). */
642
+ setAgentOptions(opts: { sandboxProfile?: string; grokMemory?: string }): void {
643
+ if (opts.sandboxProfile !== undefined) this.opts.sandboxProfile = opts.sandboxProfile;
644
+ if (opts.grokMemory !== undefined) this.opts.grokMemory = opts.grokMemory;
645
+ }
646
+
625
647
  stop(): void {
626
648
  this.stopped = true;
627
649
  if (this.restartTimer) {
@@ -780,11 +802,13 @@ export class GrokClient extends EventEmitter {
780
802
  (planSnippet ? ` plan=${planSnippet.replace(/\s+/g, " ").slice(0, 80)}` : "") +
781
803
  (keys ? ` keys=[${keys}]` : ""),
782
804
  );
783
- result = autoApproveExitPlanMode(params);
805
+ result = this.planExitHandler
806
+ ? await this.planExitHandler(params)
807
+ : autoApproveExitPlanMode(params);
784
808
  } else if (isAskUserQuestionMethod(method)) {
785
- // No TUI question form: skip so the agent continues (prefer later Telegram UI).
786
- log.info(`auto-skipping ${method} (no interactive question UI in Telegram bridge)`);
787
- result = autoSkipAskUserQuestion(params);
809
+ result = this.askUserHandler
810
+ ? await this.askUserHandler(params)
811
+ : autoSkipAskUserQuestion(params);
788
812
  } else {
789
813
  // We advertise no fs/terminal capabilities, so the agent shouldn't ask.
790
814
  // Log at warn — unknown reverse methods used to silently break plan exit
@@ -24,6 +24,14 @@ export const EXT_EXIT_PLAN_MODE_ALT = "x.ai/exit_plan_mode";
24
24
  export const EXT_ASK_USER_QUESTION = "_x.ai/ask_user_question";
25
25
  export const EXT_ASK_USER_QUESTION_ALT = "x.ai/ask_user_question";
26
26
 
27
+ /** Plan approval outcomes accepted by Grok Build's ExitPlanModeExtResponse. */
28
+ export type PlanExitOutcome = "approved" | "abandoned" | "request_changes";
29
+
30
+ export interface PlanExitDecision {
31
+ outcome: PlanExitOutcome;
32
+ feedback?: string;
33
+ }
34
+
27
35
  /**
28
36
  * Auto-approve leaving plan mode so the agent can implement.
29
37
  *
package/src/index.ts CHANGED
@@ -146,6 +146,10 @@ async function main(): Promise<void> {
146
146
  model: cfg.grokModel,
147
147
  autoRestart: cfg.grokAutoRestart,
148
148
  promptIdleTimeoutMs: cfg.promptIdleMs,
149
+ sandboxProfile: cfg.sandboxProfile,
150
+ grokMemory: cfg.grokMemory,
151
+ agentProfile: cfg.agentProfile,
152
+ pluginDir: cfg.pluginDir,
149
153
  });
150
154
 
151
155
  // Retry ACP connect — agent crash at boot should not kill the Telegram bot.
@@ -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