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.
- package/.env.example +13 -0
- package/CHANGELOG.md +106 -0
- package/README.md +20 -5
- package/docs/GROUP.md +39 -4
- package/docs/INSTALL.md +2 -0
- package/package.json +4 -4
- package/scripts/setup.mjs +20 -3
- package/src/app/instance.ts +223 -0
- package/src/app/types.ts +34 -1
- package/src/bot/ask-user-service.ts +226 -0
- package/src/bot/auth.ts +5 -1
- package/src/bot/bot.ts +105 -4
- package/src/bot/chat-controller.ts +129 -0
- package/src/bot/commands.ts +22 -2
- package/src/bot/group-memory.ts +192 -12
- package/src/bot/handlers/forum.ts +16 -6
- package/src/bot/handlers/grok-slash.ts +336 -0
- package/src/bot/handlers/message.ts +165 -25
- package/src/bot/handlers/photo.ts +4 -1
- package/src/bot/handlers/system.ts +63 -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/plan-exit-service.ts +169 -0
- package/src/bot/prompt-anchor.ts +2 -3
- package/src/bot/prompt-content.ts +5 -0
- package/src/bot/registry.ts +11 -2
- package/src/bot/scope.ts +9 -8
- package/src/bot/session-runtime.ts +665 -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/cli.ts +43 -7
- package/src/config.ts +35 -25
- package/src/forum/manager.ts +2 -1
- package/src/forum/thread.ts +33 -0
- package/src/grok/client.ts +29 -5
- package/src/grok/plan-approval.ts +8 -0
- package/src/index.ts +4 -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/service/linux.ts +21 -15
- package/src/service/macos.ts +20 -15
- package/src/service/platform.ts +12 -3
- package/src/service/types.ts +6 -0
- package/src/service/windows.ts +31 -22
- package/src/sessions/history.ts +18 -0
- package/src/stream/streamer.ts +46 -10
|
@@ -23,7 +23,7 @@ import { type PromptInput, type ReasoningEffort, textPrompt } from "../app/types
|
|
|
23
23
|
import { createLogger } from "../logger.js";
|
|
24
24
|
import { buildTranscript, readHistory } from "../sessions/history.js";
|
|
25
25
|
import { sessionHashtags } from "../render/hashtags.js";
|
|
26
|
-
import { PROGRESS_DIRECTIVE } from "../render/progress.js";
|
|
26
|
+
import { extractProgress, PROGRESS_DIRECTIVE } from "../render/progress.js";
|
|
27
27
|
import { buildPriming, recentTranscript } from "./session-fork.js";
|
|
28
28
|
import { TailWatcher } from "../sessions/tail.js";
|
|
29
29
|
import type { HistoryEntry } from "../sessions/types.js";
|
|
@@ -63,14 +63,30 @@ import {
|
|
|
63
63
|
suggestionsKeyboard,
|
|
64
64
|
} from "./suggestions.js";
|
|
65
65
|
import type { ForumManager } from "../forum/manager.js";
|
|
66
|
+
import { isGeneralThread, outboundThreadExtra } from "../forum/thread.js";
|
|
66
67
|
import type { SessionStore } from "../sessions/store.js";
|
|
67
68
|
import type { TelegramBotService } from "./telegram-bots.js";
|
|
68
69
|
import { executeTelegramActions } from "./telegram-actions.js";
|
|
70
|
+
import {
|
|
71
|
+
buildManagerContextBlock,
|
|
72
|
+
injectManagerContext,
|
|
73
|
+
} from "./manager-context.js";
|
|
74
|
+
import {
|
|
75
|
+
bindJobSession,
|
|
76
|
+
updateManagerJob,
|
|
77
|
+
type ReportBackMeta,
|
|
78
|
+
} from "./manager-jobs.js";
|
|
79
|
+
import {
|
|
80
|
+
buildManagerWorkReportPrompt,
|
|
81
|
+
isManagerWorkReportPrompt,
|
|
82
|
+
wrapManagerDirective,
|
|
83
|
+
} from "../render/manager-directive.js";
|
|
69
84
|
import {
|
|
70
85
|
buildTelegramBridgeDirective,
|
|
71
86
|
buildTelegramBridgeResultsPrompt,
|
|
72
87
|
extractTelegramActions,
|
|
73
88
|
isTelegramBridgeResultsPrompt,
|
|
89
|
+
stripTelegramActionFences,
|
|
74
90
|
wrapTelegramBridgePrompt,
|
|
75
91
|
} from "../render/telegram-bridge.js";
|
|
76
92
|
import {
|
|
@@ -206,7 +222,23 @@ export class SessionRuntime {
|
|
|
206
222
|
bots: TelegramBotService;
|
|
207
223
|
/** Cross-topic prompt dispatch (create_topic → send_prompt orchestration). */
|
|
208
224
|
submitTopicPrompt?: import("./telegram-actions.js").SubmitTopicPromptFn;
|
|
225
|
+
/** Wake General manager with a work-report prompt. */
|
|
226
|
+
wakeManager?: (opts: {
|
|
227
|
+
originChatId: number;
|
|
228
|
+
originThreadId: number;
|
|
229
|
+
prompt: string;
|
|
230
|
+
}) => Promise<void>;
|
|
209
231
|
};
|
|
232
|
+
/**
|
|
233
|
+
* Report-back for the *current* turn chain (dispatch + recheck/suggestions).
|
|
234
|
+
* Set from PromptInput.reportBack at turn start; kept until queue drains.
|
|
235
|
+
*/
|
|
236
|
+
private pendingReportBack: ReportBackMeta | undefined;
|
|
237
|
+
/**
|
|
238
|
+
* Staged by {@link setReportBack} and attached to the next {@link submit}
|
|
239
|
+
* so concurrent dispatches carry their own job through the queue.
|
|
240
|
+
*/
|
|
241
|
+
private stagedReportBack: ReportBackMeta | undefined;
|
|
210
242
|
/** Last credits total reported for this session (for per-turn delta accounting). */
|
|
211
243
|
private lastReportedCredits = 0;
|
|
212
244
|
/** Live "what is happening now" line while a turn is in flight (tools/plan). */
|
|
@@ -227,6 +259,15 @@ export class SessionRuntime {
|
|
|
227
259
|
/** Quiet meta capture (suggestions) — never stream to Telegram. */
|
|
228
260
|
private capturingQuiet = false;
|
|
229
261
|
private quietCaptureBuf = "";
|
|
262
|
+
/**
|
|
263
|
+
* General manager: Thinking… / Starting… bubble for this turn (deleted when
|
|
264
|
+
* silent, or replaced by a single fallback reply).
|
|
265
|
+
*/
|
|
266
|
+
private managerStatusMsgId: number | undefined;
|
|
267
|
+
/** Count of successful notify actions this turn (user-facing messages). */
|
|
268
|
+
private managerNotifyCount = 0;
|
|
269
|
+
/** True when this turn already delivered at least one user-visible message. */
|
|
270
|
+
private managerUserVisible = false;
|
|
230
271
|
/**
|
|
231
272
|
* Done delivery bookkeeping for this turn: expect a loud Done ping, and whether
|
|
232
273
|
* one was successfully sent (finally forces a short Done if expected but missing).
|
|
@@ -269,6 +310,15 @@ export class SessionRuntime {
|
|
|
269
310
|
readonly messageThreadId: number | undefined;
|
|
270
311
|
/** Settings storage key (`chatId` or `chatId:t{threadId}`). */
|
|
271
312
|
readonly settingsKey: string;
|
|
313
|
+
/**
|
|
314
|
+
* General topic only: OpenClaw-style manager chat (orchestrate, no coding UX).
|
|
315
|
+
*/
|
|
316
|
+
readonly managerMode: boolean;
|
|
317
|
+
/**
|
|
318
|
+
* Optional: register Telegram message id → session for General reply-routing
|
|
319
|
+
* (user message, Done, stream bubbles). Wired by ChatController.
|
|
320
|
+
*/
|
|
321
|
+
onTelegramMessageBound: ((messageId: number, sessionId: string) => void) | undefined;
|
|
272
322
|
|
|
273
323
|
constructor(
|
|
274
324
|
private readonly api: Api,
|
|
@@ -286,6 +336,9 @@ export class SessionRuntime {
|
|
|
286
336
|
) {
|
|
287
337
|
this.messageThreadId = init?.messageThreadId;
|
|
288
338
|
this.settingsKey = init?.settingsKey ?? String(chatId);
|
|
339
|
+
// Only the forum General topic is the manager — not AI Chat / private DMs.
|
|
340
|
+
this.managerMode =
|
|
341
|
+
this.messageThreadId !== undefined && isGeneralThread(this.messageThreadId);
|
|
289
342
|
if (init) {
|
|
290
343
|
this.cwd = init.cwd;
|
|
291
344
|
this.projectName = init.projectName;
|
|
@@ -328,9 +381,27 @@ export class SessionRuntime {
|
|
|
328
381
|
|
|
329
382
|
/** Latest task-completion % (0–100) parsed this turn, or undefined if none. */
|
|
330
383
|
get taskProgress(): number | undefined {
|
|
384
|
+
// Manager chat never shows a progress bar.
|
|
385
|
+
if (this.managerMode) return undefined;
|
|
331
386
|
return this.progress;
|
|
332
387
|
}
|
|
333
388
|
|
|
389
|
+
/**
|
|
390
|
+
* Stage report-back for the next {@link submit} (General → project dispatch).
|
|
391
|
+
* Attached to that prompt so a second dispatch cannot steal the first job's
|
|
392
|
+
* completion report when the child session is busy/queued.
|
|
393
|
+
*/
|
|
394
|
+
setReportBack(meta: ReportBackMeta): void {
|
|
395
|
+
if (this.stagedReportBack && this.stagedReportBack.jobId !== meta.jobId) {
|
|
396
|
+
updateManagerJob(this.stagedReportBack.jobId, {
|
|
397
|
+
status: "cancelled",
|
|
398
|
+
resultSummary: "superseded by a newer manager dispatch before start",
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
this.stagedReportBack = meta;
|
|
402
|
+
if (this.sessionId) bindJobSession(meta.jobId, this.sessionId);
|
|
403
|
+
}
|
|
404
|
+
|
|
334
405
|
/**
|
|
335
406
|
* Full plan board for the live stream / status panel (above the progress bar).
|
|
336
407
|
* Empty when no plan is active this turn.
|
|
@@ -463,8 +534,8 @@ export class SessionRuntime {
|
|
|
463
534
|
if (value) {
|
|
464
535
|
// A turn was started here and is still in flight, but its streamer was
|
|
465
536
|
// finalized when we went background. Recreate it and let onUpdate feed
|
|
466
|
-
// the remaining chunks/thoughts/tools just like a normal live turn
|
|
467
|
-
//
|
|
537
|
+
// the remaining chunks/thoughts/tools just like a normal live turn.
|
|
538
|
+
// Recreate the streamer for the live turn (manager: prose-only).
|
|
468
539
|
if (this.busy && !this.streamer) {
|
|
469
540
|
// Any transient follow-watch of this session is now superseded.
|
|
470
541
|
if (this.watchIsFollow) this.stopWatch();
|
|
@@ -474,10 +545,11 @@ export class SessionRuntime {
|
|
|
474
545
|
this.cfg.streamThrottleMs,
|
|
475
546
|
this.turnReplyTo,
|
|
476
547
|
this.hashtags(),
|
|
477
|
-
(pct) => this.setProgress(pct),
|
|
478
|
-
this.cfg.progressFallback,
|
|
548
|
+
this.managerMode ? undefined : (pct) => this.setProgress(pct),
|
|
549
|
+
this.managerMode ? false : this.cfg.progressFallback,
|
|
479
550
|
this.turnStartedAt,
|
|
480
551
|
this.messageThreadId,
|
|
552
|
+
this.managerMode ? { proseOnly: true, showProgressBar: false } : undefined,
|
|
481
553
|
);
|
|
482
554
|
// Restore the live plan board so steps stay visible above the progress bar.
|
|
483
555
|
if (this.planEntries?.length) {
|
|
@@ -488,6 +560,7 @@ export class SessionRuntime {
|
|
|
488
560
|
} else {
|
|
489
561
|
this.typing.stop();
|
|
490
562
|
this.stopWatch();
|
|
563
|
+
// Seal live bubble when demoted (manager has no streamer).
|
|
491
564
|
if (this.streamer) {
|
|
492
565
|
// Finalize off the critical path so project/session switches never wait
|
|
493
566
|
// on Telegram edits of the previous live stream.
|
|
@@ -703,14 +776,22 @@ export class SessionRuntime {
|
|
|
703
776
|
|
|
704
777
|
async submit(input: PromptInput): Promise<"ran" | "queued"> {
|
|
705
778
|
await this.ensureSession();
|
|
779
|
+
// Attach staged manager report-back to this prompt (FIFO through the queue).
|
|
780
|
+
let toSubmit = input;
|
|
781
|
+
if (this.stagedReportBack) {
|
|
782
|
+
const rb = this.stagedReportBack;
|
|
783
|
+
this.stagedReportBack = undefined;
|
|
784
|
+
toSubmit = input.reportBack ? input : { ...input, reportBack: rb };
|
|
785
|
+
if (this.sessionId) bindJobSession(rb.jobId, this.sessionId);
|
|
786
|
+
}
|
|
706
787
|
if (this.busy) {
|
|
707
|
-
this.queue.push(
|
|
788
|
+
this.queue.push(toSubmit);
|
|
708
789
|
this.changed();
|
|
709
790
|
return "queued";
|
|
710
791
|
}
|
|
711
792
|
// First-prompt steering is applied inside runTurn so queued first messages
|
|
712
793
|
// (and flushQueue) get the same complexity + telegram bridge directives.
|
|
713
|
-
void this.runTurn(
|
|
794
|
+
void this.runTurn(toSubmit);
|
|
714
795
|
return "ran";
|
|
715
796
|
}
|
|
716
797
|
|
|
@@ -726,16 +807,26 @@ export class SessionRuntime {
|
|
|
726
807
|
topicGroupId: this.cfg.topicGroupId,
|
|
727
808
|
allowedBots: this.cfg.allowedTelegramBots,
|
|
728
809
|
botCommands: this.cfg.telegramBotCommands,
|
|
810
|
+
managerMode: this.managerMode,
|
|
729
811
|
});
|
|
730
812
|
}
|
|
731
813
|
|
|
732
814
|
/**
|
|
733
815
|
* Complexity + telegram bridge teaching on the first prompt of a brand-new
|
|
734
816
|
* conversation only (no prior user turns in this process / session jsonl).
|
|
817
|
+
* Manager mode uses MANAGER_DIRECTIVE instead of complexity/progress coding UX.
|
|
735
818
|
*/
|
|
736
819
|
private applyFirstPromptSteering(input: PromptInput): PromptInput {
|
|
737
820
|
if (!this.shouldSteerFirstPrompt(input)) return input;
|
|
738
|
-
let toRun
|
|
821
|
+
let toRun: PromptInput;
|
|
822
|
+
if (this.managerMode) {
|
|
823
|
+
toRun = wrapManagerDirective(input);
|
|
824
|
+
toRun = wrapTelegramBridgePrompt(toRun, this.telegramBridgeDirective());
|
|
825
|
+
this.markFirstPromptSteered();
|
|
826
|
+
log.info(`chat ${this.chatId}: first-prompt manager + telegram bridge applied`);
|
|
827
|
+
return toRun;
|
|
828
|
+
}
|
|
829
|
+
toRun = wrapAutoComplexityPrompt(input);
|
|
739
830
|
toRun = wrapTelegramBridgePrompt(toRun, this.telegramBridgeDirective());
|
|
740
831
|
this.markFirstPromptSteered();
|
|
741
832
|
log.info(`chat ${this.chatId}: first-prompt complexity + telegram bridge applied`);
|
|
@@ -755,8 +846,10 @@ export class SessionRuntime {
|
|
|
755
846
|
// Never wrap meta follow-ups even if somehow first.
|
|
756
847
|
if (
|
|
757
848
|
input.skipSelfRecheck ||
|
|
849
|
+
input.rawSlashCommand ||
|
|
758
850
|
isSelfRecheckPrompt(input.text) ||
|
|
759
|
-
isTelegramBridgeResultsPrompt(input.text)
|
|
851
|
+
isTelegramBridgeResultsPrompt(input.text) ||
|
|
852
|
+
isManagerWorkReportPrompt(input.text)
|
|
760
853
|
) {
|
|
761
854
|
return false;
|
|
762
855
|
}
|
|
@@ -774,6 +867,31 @@ export class SessionRuntime {
|
|
|
774
867
|
return true;
|
|
775
868
|
}
|
|
776
869
|
|
|
870
|
+
/** Memory + topic catalog inject for every real manager user turn. */
|
|
871
|
+
private applyManagerContext(input: PromptInput): PromptInput {
|
|
872
|
+
if (!this.managerMode) return input;
|
|
873
|
+
if (
|
|
874
|
+
input.rawSlashCommand ||
|
|
875
|
+
isTelegramBridgeResultsPrompt(input.text) ||
|
|
876
|
+
isManagerWorkReportPrompt(input.text) ||
|
|
877
|
+
isSelfRecheckPrompt(input.text)
|
|
878
|
+
) {
|
|
879
|
+
return input;
|
|
880
|
+
}
|
|
881
|
+
if (!this.bridge) return input;
|
|
882
|
+
const userText = stripDirectiveWrappers(input.text) || input.text;
|
|
883
|
+
const block = buildManagerContextBlock({
|
|
884
|
+
userText,
|
|
885
|
+
sessionsDir: this.cfg.sessionsDir,
|
|
886
|
+
store: this.bridge.store,
|
|
887
|
+
forum: this.bridge.forum,
|
|
888
|
+
});
|
|
889
|
+
return {
|
|
890
|
+
...input,
|
|
891
|
+
text: injectManagerContext(input.text, block),
|
|
892
|
+
};
|
|
893
|
+
}
|
|
894
|
+
|
|
777
895
|
/**
|
|
778
896
|
* Stop the current turn for this runtime only.
|
|
779
897
|
* Soft ACP cancel + session-scoped force-complete; never kills the shared
|
|
@@ -903,7 +1021,12 @@ export class SessionRuntime {
|
|
|
903
1021
|
private async runTurn(input: PromptInput): Promise<void> {
|
|
904
1022
|
// Apply before any turn bookkeeping so card previews / logs see the wrapped
|
|
905
1023
|
// text the same way the agent does (also covers flushQueue first messages).
|
|
906
|
-
|
|
1024
|
+
try {
|
|
1025
|
+
input = this.applyFirstPromptSteering(input);
|
|
1026
|
+
input = this.applyManagerContext(input);
|
|
1027
|
+
} catch (e) {
|
|
1028
|
+
log.warn(`prompt steering/context failed: ${(e as Error).message}`);
|
|
1029
|
+
}
|
|
907
1030
|
|
|
908
1031
|
this.busy = true;
|
|
909
1032
|
this.cancelled = false;
|
|
@@ -915,19 +1038,29 @@ export class SessionRuntime {
|
|
|
915
1038
|
this.turnExpectDone = false;
|
|
916
1039
|
this.turnDonePinged = false;
|
|
917
1040
|
this.isSelfRecheckTurn = isSelfRecheckPrompt(input.text);
|
|
918
|
-
// Meta turns (recheck, bridge results,
|
|
1041
|
+
// Meta turns (recheck, bridge results, work reports) never arm another recheck.
|
|
919
1042
|
const isBridgeResults = isTelegramBridgeResultsPrompt(input.text);
|
|
1043
|
+
const isWorkReport = isManagerWorkReportPrompt(input.text);
|
|
1044
|
+
// Keep the Thinking… bubble across search_memory → results follow-ups so
|
|
1045
|
+
// the user is not left with a deleted placeholder and no reply.
|
|
1046
|
+
if (this.managerStatusMsgId !== undefined && !isBridgeResults) {
|
|
1047
|
+
const orphan = this.managerStatusMsgId;
|
|
1048
|
+
this.managerStatusMsgId = undefined;
|
|
1049
|
+
void this.deleteManagerStatus(orphan);
|
|
1050
|
+
}
|
|
920
1051
|
this.skipSelfRecheck =
|
|
921
1052
|
!!input.skipSelfRecheck ||
|
|
922
1053
|
this.isSelfRecheckTurn ||
|
|
923
|
-
isBridgeResults
|
|
1054
|
+
isBridgeResults ||
|
|
1055
|
+
isWorkReport ||
|
|
1056
|
+
this.managerMode;
|
|
924
1057
|
// Fresh user work resets bridge-chain depth + suggestion anchors.
|
|
925
|
-
if (!this.isSelfRecheckTurn && !isBridgeResults) {
|
|
1058
|
+
if (!this.isSelfRecheckTurn && !isBridgeResults && !isWorkReport) {
|
|
926
1059
|
this.bridgeResultDepth = 0;
|
|
927
1060
|
}
|
|
928
1061
|
// Fresh user work resets suggestion anchors; recheck / bridge results keep the original ask.
|
|
929
1062
|
// Strip complexity/reply wrappers so suggestions + recheck see the real ask.
|
|
930
|
-
if (!this.isSelfRecheckTurn && !isBridgeResults) {
|
|
1063
|
+
if (!this.isSelfRecheckTurn && !isBridgeResults && !isWorkReport) {
|
|
931
1064
|
this.suggestionUserText = stripDirectiveWrappers(input.text) || input.text;
|
|
932
1065
|
this.preRecheckAssistantText = "";
|
|
933
1066
|
this.preRecheckFileOps = new Map();
|
|
@@ -942,6 +1075,15 @@ export class SessionRuntime {
|
|
|
942
1075
|
this.setSessionComment(preview);
|
|
943
1076
|
}
|
|
944
1077
|
}
|
|
1078
|
+
// Bind THIS prompt's report-back (manager dispatch). Meta follow-ups
|
|
1079
|
+
// (recheck / bridge results) omit reportBack and keep the prior job until
|
|
1080
|
+
// the queue drains and we report once.
|
|
1081
|
+
if (input.reportBack) {
|
|
1082
|
+
this.pendingReportBack = input.reportBack as ReportBackMeta;
|
|
1083
|
+
}
|
|
1084
|
+
if (this.pendingReportBack && this.sessionId) {
|
|
1085
|
+
bindJobSession(this.pendingReportBack.jobId, this.sessionId);
|
|
1086
|
+
}
|
|
945
1087
|
this.shownToolIds = new Set();
|
|
946
1088
|
this.toolCallCache = new Map();
|
|
947
1089
|
this.fileOps = new Map();
|
|
@@ -961,9 +1103,33 @@ export class SessionRuntime {
|
|
|
961
1103
|
// A new streamed turn supersedes any transient "follow" watch of this same
|
|
962
1104
|
// session's previous in-flight turn (avoids duplicated output).
|
|
963
1105
|
if (this.watchIsFollow) this.stopWatch();
|
|
964
|
-
|
|
1106
|
+
// Manager (General): stream short chat prose (no tools/progress). Work
|
|
1107
|
+
// reports stay quiet. Bridge-result follow-ups ARE user-facing — that is
|
|
1108
|
+
// usually when the manager answers after search_memory.
|
|
1109
|
+
const managerSilent =
|
|
1110
|
+
this.managerMode && (isWorkReport || this.isSelfRecheckTurn);
|
|
1111
|
+
const live = this.foreground && !managerSilent;
|
|
965
1112
|
const startedAt = Date.now();
|
|
966
1113
|
this.turnStartedAt = startedAt;
|
|
1114
|
+
this.managerNotifyCount = 0;
|
|
1115
|
+
this.managerUserVisible = false;
|
|
1116
|
+
// General: Starting… (from message handler) → Thinking… then stream into it.
|
|
1117
|
+
// Work-report wakes stay fully silent (no bubble).
|
|
1118
|
+
const managerMeta = managerSilent;
|
|
1119
|
+
let thinkingMsgId: number | undefined = input.seedMessageId ?? this.managerStatusMsgId;
|
|
1120
|
+
if (this.managerMode && !managerSilent && this.turnReplyTo !== undefined) {
|
|
1121
|
+
if (thinkingMsgId !== undefined) {
|
|
1122
|
+
await this.editManagerStatus(thinkingMsgId, "Thinking\u2026");
|
|
1123
|
+
} else {
|
|
1124
|
+
thinkingMsgId = await this.postManagerStatus(this.turnReplyTo, "Thinking\u2026");
|
|
1125
|
+
}
|
|
1126
|
+
this.managerStatusMsgId = thinkingMsgId;
|
|
1127
|
+
} else if (this.managerMode && managerSilent && thinkingMsgId !== undefined && !isBridgeResults) {
|
|
1128
|
+
// Drop Starting… leftover on silent work-report / recheck turns.
|
|
1129
|
+
await this.deleteManagerStatus(thinkingMsgId);
|
|
1130
|
+
thinkingMsgId = undefined;
|
|
1131
|
+
this.managerStatusMsgId = undefined;
|
|
1132
|
+
}
|
|
967
1133
|
this.streamer = live
|
|
968
1134
|
? new ResponseStreamer(
|
|
969
1135
|
this.api,
|
|
@@ -971,13 +1137,30 @@ export class SessionRuntime {
|
|
|
971
1137
|
this.cfg.streamThrottleMs,
|
|
972
1138
|
this.turnReplyTo,
|
|
973
1139
|
this.hashtags(),
|
|
974
|
-
(pct) => this.setProgress(pct),
|
|
975
|
-
this.cfg.progressFallback,
|
|
1140
|
+
this.managerMode ? undefined : (pct) => this.setProgress(pct),
|
|
1141
|
+
this.managerMode ? false : this.cfg.progressFallback,
|
|
976
1142
|
startedAt,
|
|
977
1143
|
this.messageThreadId,
|
|
1144
|
+
this.managerMode
|
|
1145
|
+
? {
|
|
1146
|
+
proseOnly: true,
|
|
1147
|
+
showProgressBar: false,
|
|
1148
|
+
seedMessageId: thinkingMsgId,
|
|
1149
|
+
}
|
|
1150
|
+
: undefined,
|
|
978
1151
|
)
|
|
979
1152
|
: undefined;
|
|
980
|
-
|
|
1153
|
+
// Bind user message (+ status bubble) → session for reply routing.
|
|
1154
|
+
if (this.managerMode && this.sessionId) {
|
|
1155
|
+
if (this.turnReplyTo !== undefined) {
|
|
1156
|
+
this.onTelegramMessageBound?.(this.turnReplyTo, this.sessionId);
|
|
1157
|
+
}
|
|
1158
|
+
if (thinkingMsgId !== undefined) {
|
|
1159
|
+
this.onTelegramMessageBound?.(thinkingMsgId, this.sessionId);
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
// Typing indicator for user-facing manager turns and live project streams.
|
|
1163
|
+
if (live || (this.managerMode && !managerMeta)) this.typing.start();
|
|
981
1164
|
this.activity(true);
|
|
982
1165
|
this.changed();
|
|
983
1166
|
this.imageScanText = "";
|
|
@@ -986,8 +1169,13 @@ export class SessionRuntime {
|
|
|
986
1169
|
const content = buildContentBlocks(input, {
|
|
987
1170
|
reasoning: reasoningDirective(this.reasoning),
|
|
988
1171
|
priming: this.primingContext,
|
|
989
|
-
imageOutput:
|
|
990
|
-
|
|
1172
|
+
imageOutput:
|
|
1173
|
+
!this.managerMode && this.cfg.sendAgentImages
|
|
1174
|
+
? IMAGE_OUTPUT_DIRECTIVE
|
|
1175
|
+
: undefined,
|
|
1176
|
+
// Manager chat: no progress spam; project topics keep the usual directive.
|
|
1177
|
+
progress:
|
|
1178
|
+
!this.managerMode && this.cfg.showProgress ? PROGRESS_DIRECTIVE : undefined,
|
|
991
1179
|
});
|
|
992
1180
|
this.primingContext = undefined;
|
|
993
1181
|
|
|
@@ -1054,8 +1242,8 @@ export class SessionRuntime {
|
|
|
1054
1242
|
// spam to the chat (live step / status panel only).
|
|
1055
1243
|
const pingDone =
|
|
1056
1244
|
canPing && (this.foreground || !hasQueued) && !queuedBridgeResults;
|
|
1057
|
-
//
|
|
1058
|
-
this.turnExpectDone = pingDone;
|
|
1245
|
+
// Manager uses notify/finishManagerUserFacing — never arm Done safety-net spam.
|
|
1246
|
+
this.turnExpectDone = pingDone && !this.managerMode;
|
|
1059
1247
|
|
|
1060
1248
|
// One-shot self-recheck: only after a real *user* turn (not meta/auto),
|
|
1061
1249
|
// with idle queue. skipSelfRecheck blocks loops after recheck / auto-batch.
|
|
@@ -1120,16 +1308,47 @@ export class SessionRuntime {
|
|
|
1120
1308
|
}
|
|
1121
1309
|
}
|
|
1122
1310
|
|
|
1311
|
+
// Manager: keep Thinking… while bridge results are chaining so the
|
|
1312
|
+
// follow-up turn can stream the real answer into the same bubble.
|
|
1313
|
+
if (this.managerMode && queuedBridgeResults && this.managerStatusMsgId !== undefined) {
|
|
1314
|
+
void this.editManagerStatus(this.managerStatusMsgId, "Thinking\u2026");
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1123
1317
|
if (!queuedRecheck && !queuedBridgeResults) {
|
|
1318
|
+
// Manager (General): quiet-by-default completion — no Done spam.
|
|
1319
|
+
// User-facing only via notify (already sent) or one fallback reply.
|
|
1320
|
+
if (this.managerMode) {
|
|
1321
|
+
await this.finishManagerUserFacing(managerMeta, final.result?.stopReason, startedAt);
|
|
1322
|
+
this.turnDonePinged = true;
|
|
1323
|
+
// Suggestions only when something was actually shown to the user.
|
|
1324
|
+
if (
|
|
1325
|
+
final.result &&
|
|
1326
|
+
!this.cancelled &&
|
|
1327
|
+
!hasQueued &&
|
|
1328
|
+
this.managerUserVisible &&
|
|
1329
|
+
!managerMeta
|
|
1330
|
+
) {
|
|
1331
|
+
try {
|
|
1332
|
+
const baseForSug =
|
|
1333
|
+
cleanManagerVisibleText(this.turnAssistantText).slice(0, 500) || "Done";
|
|
1334
|
+
await this.collectAndApplySuggestions(baseForSug, undefined, {
|
|
1335
|
+
autoQueue: true,
|
|
1336
|
+
});
|
|
1337
|
+
} catch (e) {
|
|
1338
|
+
log.debug(`manager suggestions failed: ${(e as Error).message}`);
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
} else {
|
|
1124
1342
|
// Build Done text *now* (after quiet decision) so a cancel during the
|
|
1125
1343
|
// recheck-decision wait shows ⏹ Stopped, not a stale ✅ Done head.
|
|
1126
1344
|
let doneText = this.isSelfRecheckTurn
|
|
1127
|
-
|
|
1128
|
-
|
|
1345
|
+
? this.completionMessageSplit(final.result?.stopReason, startedAt, streamedOutput)
|
|
1346
|
+
: this.completionMessage(final.result?.stopReason, startedAt, streamedOutput);
|
|
1129
1347
|
// 1) Always send Done FIRST — never block the completion ping on the
|
|
1130
1348
|
// quiet suggestions prompt (which can hang and look like "no Done").
|
|
1131
1349
|
let doneMsgId: number | undefined;
|
|
1132
|
-
|
|
1350
|
+
const shouldPingDone = pingDone;
|
|
1351
|
+
if (shouldPingDone && doneText.trim()) {
|
|
1133
1352
|
doneMsgId = await this.notify(doneText, {
|
|
1134
1353
|
loud: true,
|
|
1135
1354
|
replyTo: this.turnReplyTo,
|
|
@@ -1137,30 +1356,51 @@ export class SessionRuntime {
|
|
|
1137
1356
|
});
|
|
1138
1357
|
if (doneMsgId !== undefined) this.turnDonePinged = true;
|
|
1139
1358
|
}
|
|
1140
|
-
// 2) Suggestions
|
|
1359
|
+
// 2) Suggestions: project topics keep Done-edit UX.
|
|
1141
1360
|
if (final.result && !this.cancelled && !hasQueued) {
|
|
1142
1361
|
try {
|
|
1143
|
-
const
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1362
|
+
const baseForSug = doneText;
|
|
1363
|
+
const sug = await this.collectAndApplySuggestions(
|
|
1364
|
+
baseForSug,
|
|
1365
|
+
switchKb,
|
|
1366
|
+
{
|
|
1367
|
+
autoQueue: this.foreground,
|
|
1368
|
+
},
|
|
1369
|
+
);
|
|
1370
|
+
if (shouldPingDone && sug.text !== doneText) {
|
|
1150
1371
|
await this.enhanceDoneMessage(doneMsgId, sug.text, sug.markup ?? switchKb);
|
|
1151
1372
|
}
|
|
1152
1373
|
} catch (e) {
|
|
1153
1374
|
log.debug(`suggestions after Done failed: ${(e as Error).message}`);
|
|
1154
1375
|
}
|
|
1155
1376
|
}
|
|
1377
|
+
}
|
|
1156
1378
|
// Clear frozen first-turn ops after final Done (recheck path done).
|
|
1157
1379
|
if (this.isSelfRecheckTurn) this.preRecheckFileOps = new Map();
|
|
1380
|
+
|
|
1381
|
+
// Child work dispatched from General → wake manager with a report.
|
|
1382
|
+
// (Waits only for same-job meta follow-ups; see maybeReportBackToManager.)
|
|
1383
|
+
await this.maybeReportBackToManager({
|
|
1384
|
+
ok: !!final.result && !this.cancelled,
|
|
1385
|
+
cancelled: this.cancelled,
|
|
1386
|
+
stopReason: final.result?.stopReason,
|
|
1387
|
+
error: undefined,
|
|
1388
|
+
});
|
|
1158
1389
|
}
|
|
1159
1390
|
// queuedBridgeResults: stay quiet in chat — agent gets results via queue.
|
|
1160
1391
|
} else if (final.error) {
|
|
1161
|
-
//
|
|
1162
|
-
|
|
1163
|
-
|
|
1392
|
+
// Manager: one short important message (or edit Thinking…); never Done spam.
|
|
1393
|
+
if (this.managerMode) {
|
|
1394
|
+
await this.finishManagerError(final.error.message, startedAt);
|
|
1395
|
+
this.turnDonePinged = true;
|
|
1396
|
+
await this.maybeReportBackToManager({
|
|
1397
|
+
ok: false,
|
|
1398
|
+
cancelled: false,
|
|
1399
|
+
error: final.error.message,
|
|
1400
|
+
});
|
|
1401
|
+
} else if (this.isSelfRecheckTurn && !hasQueued) {
|
|
1402
|
+
// If the self-recheck pass itself failed, still surface Done for the
|
|
1403
|
+
// original work (split files + suggestions) so the user is not stuck.
|
|
1164
1404
|
const switchKb = this.switchKeyboard();
|
|
1165
1405
|
const pingDone = canPing && (this.foreground || !hasQueued);
|
|
1166
1406
|
this.turnExpectDone = pingDone;
|
|
@@ -1189,6 +1429,13 @@ export class SessionRuntime {
|
|
|
1189
1429
|
}
|
|
1190
1430
|
}
|
|
1191
1431
|
this.preRecheckFileOps = new Map();
|
|
1432
|
+
// Self-recheck failed after real work — still report manager job if any.
|
|
1433
|
+
await this.maybeReportBackToManager({
|
|
1434
|
+
ok: true,
|
|
1435
|
+
cancelled: this.cancelled,
|
|
1436
|
+
stopReason: "self_recheck_failed",
|
|
1437
|
+
error: final.error.message,
|
|
1438
|
+
});
|
|
1192
1439
|
} else {
|
|
1193
1440
|
const transient = isTransientError(final.error);
|
|
1194
1441
|
const liveMsg = this.errorMessage(final.error, startedAt, final.attempts, transient);
|
|
@@ -1201,6 +1448,11 @@ export class SessionRuntime {
|
|
|
1201
1448
|
});
|
|
1202
1449
|
if (id !== undefined) this.turnDonePinged = true;
|
|
1203
1450
|
}
|
|
1451
|
+
await this.maybeReportBackToManager({
|
|
1452
|
+
ok: false,
|
|
1453
|
+
cancelled: false,
|
|
1454
|
+
error: final.error.message,
|
|
1455
|
+
});
|
|
1204
1456
|
}
|
|
1205
1457
|
}
|
|
1206
1458
|
} catch (err) {
|
|
@@ -1253,6 +1505,14 @@ export class SessionRuntime {
|
|
|
1253
1505
|
}
|
|
1254
1506
|
}
|
|
1255
1507
|
this.preRecheckFileOps = new Map();
|
|
1508
|
+
} else if (this.managerMode) {
|
|
1509
|
+
await this.finishManagerError(errMsg, startedAt);
|
|
1510
|
+
this.turnDonePinged = true;
|
|
1511
|
+
await this.maybeReportBackToManager({
|
|
1512
|
+
ok: false,
|
|
1513
|
+
cancelled: this.cancelled,
|
|
1514
|
+
error: errMsg,
|
|
1515
|
+
});
|
|
1256
1516
|
} else {
|
|
1257
1517
|
const msg = `\u274C Error after ${fmtDuration(Date.now() - startedAt)}: ${errMsg}`;
|
|
1258
1518
|
this.lastCompletion = msg;
|
|
@@ -1267,11 +1527,17 @@ export class SessionRuntime {
|
|
|
1267
1527
|
});
|
|
1268
1528
|
if (id !== undefined) this.turnDonePinged = true;
|
|
1269
1529
|
}
|
|
1530
|
+
await this.maybeReportBackToManager({
|
|
1531
|
+
ok: false,
|
|
1532
|
+
cancelled: this.cancelled,
|
|
1533
|
+
error: errMsg,
|
|
1534
|
+
});
|
|
1270
1535
|
}
|
|
1271
1536
|
} finally {
|
|
1272
1537
|
// Safety net: turn completed with an expected Done ping that never landed
|
|
1273
1538
|
// (notify failed, hung path, etc.). Never block queue flush on this.
|
|
1274
|
-
|
|
1539
|
+
// Manager never uses this path (turnExpectDone is false in manager mode).
|
|
1540
|
+
if (this.turnExpectDone && !this.turnDonePinged && !this.managerMode) {
|
|
1275
1541
|
const fallback =
|
|
1276
1542
|
this.lastCompletion?.trim() ||
|
|
1277
1543
|
`\u2705 Done \u00B7 ${fmtDuration(Date.now() - startedAt)}`;
|
|
@@ -1358,20 +1624,47 @@ export class SessionRuntime {
|
|
|
1358
1624
|
cfg: this.cfg,
|
|
1359
1625
|
chatId: this.chatId,
|
|
1360
1626
|
messageThreadId: this.messageThreadId,
|
|
1627
|
+
replyToMessageId: this.turnReplyTo,
|
|
1361
1628
|
forum: this.bridge.forum,
|
|
1362
1629
|
store: this.bridge.store,
|
|
1363
1630
|
bots: this.bridge.bots,
|
|
1364
1631
|
submitTopicPrompt: this.bridge.submitTopicPrompt,
|
|
1632
|
+
managerMode: this.managerMode,
|
|
1633
|
+
managerUserAskPreview: this.suggestionUserText || cleanUserPreview(this.turnUserText, 400),
|
|
1365
1634
|
});
|
|
1366
1635
|
|
|
1636
|
+
// Count successful notify actions. Do not delete the live stream bubble
|
|
1637
|
+
// (same id as Thinking…) — that would wipe the user's visible reply.
|
|
1638
|
+
for (const r of results) {
|
|
1639
|
+
if (r.action === "notify" && r.ok) {
|
|
1640
|
+
this.managerNotifyCount++;
|
|
1641
|
+
this.managerUserVisible = true;
|
|
1642
|
+
const streamLive = this.streamer?.liveMessageId;
|
|
1643
|
+
if (
|
|
1644
|
+
this.managerStatusMsgId !== undefined &&
|
|
1645
|
+
this.managerStatusMsgId !== streamLive &&
|
|
1646
|
+
!this.streamer?.hasOutput
|
|
1647
|
+
) {
|
|
1648
|
+
const sid = this.managerStatusMsgId;
|
|
1649
|
+
this.managerStatusMsgId = undefined;
|
|
1650
|
+
void this.deleteManagerStatus(sid);
|
|
1651
|
+
}
|
|
1652
|
+
const mid = (r.data as { messageId?: number } | undefined)?.messageId;
|
|
1653
|
+
if (mid !== undefined && this.sessionId) {
|
|
1654
|
+
this.onTelegramMessageBound?.(mid, this.sessionId);
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1367
1659
|
// Only announce durable side-effects in chat (topic create/bind/cross-prompt).
|
|
1368
|
-
//
|
|
1660
|
+
// Manager mode stays quiet — use notify for user text; no auto notes.
|
|
1661
|
+
// search_memory / list_* / bot wait status stay silent — results go to the agent.
|
|
1369
1662
|
const durableActions = new Set(["create_topic", "set_path", "send_prompt"]);
|
|
1370
1663
|
const notes = results
|
|
1371
1664
|
.filter((r) => durableActions.has(r.action) && r.userNote?.trim())
|
|
1372
1665
|
.map((r) => r.userNote!)
|
|
1373
1666
|
.filter(Boolean);
|
|
1374
|
-
if (notes.length > 0 && this.foreground) {
|
|
1667
|
+
if (notes.length > 0 && this.foreground && !this.managerMode) {
|
|
1375
1668
|
await this.notify(notes.join("\n"), {
|
|
1376
1669
|
loud: true,
|
|
1377
1670
|
replyTo: this.turnReplyTo,
|
|
@@ -1418,7 +1711,7 @@ export class SessionRuntime {
|
|
|
1418
1711
|
doneText: string,
|
|
1419
1712
|
switchKb: InlineKeyboard | undefined,
|
|
1420
1713
|
opts?: { autoQueue?: boolean },
|
|
1421
|
-
): Promise<{ text: string; markup?: InlineKeyboard }> {
|
|
1714
|
+
): Promise<{ text: string; markup?: InlineKeyboard; suggestions?: Suggestion[] }> {
|
|
1422
1715
|
if (!this.cfg.suggestionsEnabled || !this.sessionId) {
|
|
1423
1716
|
return { text: doneText, markup: switchKb };
|
|
1424
1717
|
}
|
|
@@ -1428,6 +1721,8 @@ export class SessionRuntime {
|
|
|
1428
1721
|
} catch (e) {
|
|
1429
1722
|
log.debug(`suggestions fetch failed: ${(e as Error).message}`);
|
|
1430
1723
|
}
|
|
1724
|
+
// Manager: keep 1–4 short follow-ups only.
|
|
1725
|
+
if (this.managerMode) suggestions = suggestions.slice(0, 4);
|
|
1431
1726
|
if (suggestions.length === 0) return { text: doneText, markup: switchKb };
|
|
1432
1727
|
|
|
1433
1728
|
const batchId = ++this.suggestionBatchSeq;
|
|
@@ -1445,12 +1740,13 @@ export class SessionRuntime {
|
|
|
1445
1740
|
let banner: string;
|
|
1446
1741
|
if (auto.length > 0) {
|
|
1447
1742
|
const batched = formatBatchedSuggestionsPrompt(auto);
|
|
1448
|
-
|
|
1743
|
+
// Hard, visible auto-approve block (especially for General chat).
|
|
1744
|
+
const lines = auto.map((s) => `\u2022 ${s.text}`);
|
|
1449
1745
|
const autoBlock =
|
|
1450
|
-
`\n\n\
|
|
1451
|
-
|
|
1746
|
+
`\n\n\u2705 Auto Approved:\n${lines.join("\n")}` +
|
|
1747
|
+
(this.managerMode ? "" : `\n(need \u2265 ${thr}%)`);
|
|
1452
1748
|
text += autoBlock;
|
|
1453
|
-
banner = `\
|
|
1749
|
+
banner = `\u2705 Auto Approved:\n${lines.join("\n")}`;
|
|
1454
1750
|
// Single queue entry — agent executes 1) 2) 3) in one turn.
|
|
1455
1751
|
// skipSelfRecheck: auto-follow-ups must not arm another recheck cycle.
|
|
1456
1752
|
this.queue.push(
|
|
@@ -1461,7 +1757,9 @@ export class SessionRuntime {
|
|
|
1461
1757
|
);
|
|
1462
1758
|
this.changed();
|
|
1463
1759
|
} else {
|
|
1464
|
-
text +=
|
|
1760
|
+
text += this.managerMode
|
|
1761
|
+
? "\n\nTap a suggestion to continue:"
|
|
1762
|
+
: "\n\n\u{1F4A1} Suggestions \u2014 tap one to continue:";
|
|
1465
1763
|
banner = "\u{1F4A1} Suggestions \u2014 tap one to continue:";
|
|
1466
1764
|
}
|
|
1467
1765
|
|
|
@@ -1469,8 +1767,168 @@ export class SessionRuntime {
|
|
|
1469
1767
|
// the same keyboard). Cleared when a new turn starts.
|
|
1470
1768
|
this.pendingSuggestions = { batchId, suggestions, banner };
|
|
1471
1769
|
|
|
1472
|
-
|
|
1473
|
-
|
|
1770
|
+
// Keyboard: show remaining (non-auto) suggestions; if all auto, no buttons.
|
|
1771
|
+
const remaining = suggestions.filter((s) => !auto.some((a) => a.text === s.text));
|
|
1772
|
+
const markup =
|
|
1773
|
+
remaining.length > 0
|
|
1774
|
+
? suggestionsKeyboard(batchId, remaining, switchKb)
|
|
1775
|
+
: switchKb;
|
|
1776
|
+
return { text, markup, suggestions };
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
/** Post General status bubble (Starting… / Thinking…) — streamer edits it later. */
|
|
1780
|
+
private async postManagerStatus(
|
|
1781
|
+
replyTo: number,
|
|
1782
|
+
text: string,
|
|
1783
|
+
): Promise<number | undefined> {
|
|
1784
|
+
try {
|
|
1785
|
+
const extra: Record<string, unknown> = {
|
|
1786
|
+
disable_notification: true,
|
|
1787
|
+
...outboundThreadExtra(this.messageThreadId),
|
|
1788
|
+
reply_parameters: {
|
|
1789
|
+
message_id: replyTo,
|
|
1790
|
+
allow_sending_without_reply: true,
|
|
1791
|
+
},
|
|
1792
|
+
};
|
|
1793
|
+
const msg = await this.api.sendMessage(this.chatId, text, extra);
|
|
1794
|
+
return msg.message_id;
|
|
1795
|
+
} catch (e) {
|
|
1796
|
+
log.debug(`manager status "${text}" failed: ${(e as Error).message}`);
|
|
1797
|
+
return undefined;
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
private async editManagerStatus(messageId: number, text: string): Promise<void> {
|
|
1802
|
+
try {
|
|
1803
|
+
await this.api.editMessageText(this.chatId, messageId, text);
|
|
1804
|
+
} catch (e) {
|
|
1805
|
+
log.debug(`manager status edit failed: ${(e as Error).message}`);
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
private async deleteManagerStatus(messageId: number): Promise<void> {
|
|
1810
|
+
try {
|
|
1811
|
+
await this.api.deleteMessage(this.chatId, messageId);
|
|
1812
|
+
} catch (e) {
|
|
1813
|
+
log.debug(`manager status delete failed: ${(e as Error).message}`);
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
/** Surface a short important error in General; clear Thinking… bubble. */
|
|
1818
|
+
private async finishManagerError(errorMessage: string, startedAt: number): Promise<void> {
|
|
1819
|
+
const elapsed = fmtDuration(Date.now() - startedAt);
|
|
1820
|
+
const short =
|
|
1821
|
+
errorMessage.length > 280 ? errorMessage.slice(0, 277) + "\u2026" : errorMessage;
|
|
1822
|
+
const text = `\u274C ${short} \u00B7 ${elapsed}`;
|
|
1823
|
+
this.lastCompletion = text;
|
|
1824
|
+
if (this.managerStatusMsgId !== undefined) {
|
|
1825
|
+
await this.editManagerStatus(this.managerStatusMsgId, text);
|
|
1826
|
+
this.managerUserVisible = true;
|
|
1827
|
+
if (this.sessionId) this.onTelegramMessageBound?.(this.managerStatusMsgId, this.sessionId);
|
|
1828
|
+
this.managerStatusMsgId = undefined;
|
|
1829
|
+
return;
|
|
1830
|
+
}
|
|
1831
|
+
if (this.turnReplyTo !== undefined) {
|
|
1832
|
+
const id = await this.notify(text, { loud: true, replyTo: this.turnReplyTo });
|
|
1833
|
+
if (id !== undefined) this.managerUserVisible = true;
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
/**
|
|
1838
|
+
* General quiet completion:
|
|
1839
|
+
* - if notify already sent → drop Thinking… bubble
|
|
1840
|
+
* - else if direct user ask and clean prose → one short fallback message
|
|
1841
|
+
* - else → delete Thinking… and stay silent
|
|
1842
|
+
*/
|
|
1843
|
+
private async finishManagerUserFacing(
|
|
1844
|
+
metaTurn: boolean,
|
|
1845
|
+
stopReason: string | undefined,
|
|
1846
|
+
startedAt: number,
|
|
1847
|
+
): Promise<void> {
|
|
1848
|
+
const elapsed = fmtDuration(Date.now() - startedAt);
|
|
1849
|
+
if (this.cancelled || stopReason === "cancelled") {
|
|
1850
|
+
this.lastCompletion = `\u23F9 Stopped \u00B7 ${elapsed}`;
|
|
1851
|
+
// Only surface cancel if user was waiting on a visible bubble.
|
|
1852
|
+
if (this.managerStatusMsgId !== undefined && !metaTurn) {
|
|
1853
|
+
await this.editManagerStatus(this.managerStatusMsgId, this.lastCompletion);
|
|
1854
|
+
this.managerUserVisible = true;
|
|
1855
|
+
} else if (this.managerStatusMsgId !== undefined) {
|
|
1856
|
+
await this.deleteManagerStatus(this.managerStatusMsgId);
|
|
1857
|
+
}
|
|
1858
|
+
this.managerStatusMsgId = undefined;
|
|
1859
|
+
return;
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
// Prose already landed in the Thinking… bubble via the streamer — keep it.
|
|
1863
|
+
if (this.streamer?.hasOutput) {
|
|
1864
|
+
this.managerUserVisible = true;
|
|
1865
|
+
this.lastCompletion =
|
|
1866
|
+
cleanManagerVisibleText(this.turnAssistantText).slice(0, 500) ||
|
|
1867
|
+
`\u2705 Done \u00B7 ${elapsed}`;
|
|
1868
|
+
this.managerStatusMsgId = undefined;
|
|
1869
|
+
return;
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
if (this.managerNotifyCount > 0) {
|
|
1873
|
+
this.lastCompletion =
|
|
1874
|
+
cleanManagerVisibleText(this.turnAssistantText).slice(0, 500) ||
|
|
1875
|
+
`\u2705 Done \u00B7 ${elapsed}`;
|
|
1876
|
+
if (this.managerStatusMsgId !== undefined) {
|
|
1877
|
+
await this.deleteManagerStatus(this.managerStatusMsgId);
|
|
1878
|
+
this.managerStatusMsgId = undefined;
|
|
1879
|
+
}
|
|
1880
|
+
return;
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1883
|
+
// Work-report / recheck: silent unless cancelled (handled above).
|
|
1884
|
+
if (metaTurn) {
|
|
1885
|
+
this.lastCompletion = `\u2705 Done \u00B7 ${elapsed}`;
|
|
1886
|
+
if (this.managerStatusMsgId !== undefined) {
|
|
1887
|
+
await this.deleteManagerStatus(this.managerStatusMsgId);
|
|
1888
|
+
this.managerStatusMsgId = undefined;
|
|
1889
|
+
}
|
|
1890
|
+
return;
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1893
|
+
// Direct user ask: single fallback if the model emitted no visible prose.
|
|
1894
|
+
const cleaned = cleanManagerVisibleText(this.turnAssistantText);
|
|
1895
|
+
const fallback = pickManagerFallbackText(cleaned);
|
|
1896
|
+
if (fallback && this.managerStatusMsgId !== undefined) {
|
|
1897
|
+
await this.editManagerStatus(this.managerStatusMsgId, fallback);
|
|
1898
|
+
this.managerUserVisible = true;
|
|
1899
|
+
this.lastCompletion = fallback.slice(0, 500);
|
|
1900
|
+
if (this.sessionId) this.onTelegramMessageBound?.(this.managerStatusMsgId, this.sessionId);
|
|
1901
|
+
this.managerStatusMsgId = undefined;
|
|
1902
|
+
return;
|
|
1903
|
+
}
|
|
1904
|
+
if (fallback && this.turnReplyTo !== undefined) {
|
|
1905
|
+
const id = await this.notify(fallback, {
|
|
1906
|
+
loud: false,
|
|
1907
|
+
replyTo: this.turnReplyTo,
|
|
1908
|
+
});
|
|
1909
|
+
if (id !== undefined) {
|
|
1910
|
+
this.managerUserVisible = true;
|
|
1911
|
+
this.lastCompletion = fallback.slice(0, 500);
|
|
1912
|
+
if (this.sessionId) this.onTelegramMessageBound?.(id, this.sessionId);
|
|
1913
|
+
}
|
|
1914
|
+
} else if (this.managerStatusMsgId !== undefined) {
|
|
1915
|
+
// Never delete the placeholder leaving the user with no reply.
|
|
1916
|
+
await this.editManagerStatus(
|
|
1917
|
+
this.managerStatusMsgId,
|
|
1918
|
+
"Working on it \u2014 I\u2019ll report back here.",
|
|
1919
|
+
);
|
|
1920
|
+
this.managerUserVisible = true;
|
|
1921
|
+
this.lastCompletion = "Working on it";
|
|
1922
|
+
if (this.sessionId) this.onTelegramMessageBound?.(this.managerStatusMsgId, this.sessionId);
|
|
1923
|
+
this.managerStatusMsgId = undefined;
|
|
1924
|
+
return;
|
|
1925
|
+
} else {
|
|
1926
|
+
this.lastCompletion = `\u2705 Done \u00B7 ${elapsed}`;
|
|
1927
|
+
}
|
|
1928
|
+
if (this.managerStatusMsgId !== undefined) {
|
|
1929
|
+
await this.deleteManagerStatus(this.managerStatusMsgId);
|
|
1930
|
+
this.managerStatusMsgId = undefined;
|
|
1931
|
+
}
|
|
1474
1932
|
}
|
|
1475
1933
|
|
|
1476
1934
|
/**
|
|
@@ -1698,7 +2156,8 @@ export class SessionRuntime {
|
|
|
1698
2156
|
reasoning: reasoningDirective(this.reasoning),
|
|
1699
2157
|
priming: this.primingContext,
|
|
1700
2158
|
imageOutput: this.cfg.sendAgentImages ? IMAGE_OUTPUT_DIRECTIVE : undefined,
|
|
1701
|
-
progress:
|
|
2159
|
+
progress:
|
|
2160
|
+
!this.managerMode && this.cfg.showProgress ? PROGRESS_DIRECTIVE : undefined,
|
|
1702
2161
|
});
|
|
1703
2162
|
this.primingContext = undefined;
|
|
1704
2163
|
log.info(
|
|
@@ -1761,7 +2220,8 @@ export class SessionRuntime {
|
|
|
1761
2220
|
reasoning: reasoningDirective(this.reasoning),
|
|
1762
2221
|
priming: transcript ? buildPriming(transcript) : undefined,
|
|
1763
2222
|
imageOutput: this.cfg.sendAgentImages ? IMAGE_OUTPUT_DIRECTIVE : undefined,
|
|
1764
|
-
progress:
|
|
2223
|
+
progress:
|
|
2224
|
+
!this.managerMode && this.cfg.showProgress ? PROGRESS_DIRECTIVE : undefined,
|
|
1765
2225
|
});
|
|
1766
2226
|
return this.runPromptWithRetries(forkContent);
|
|
1767
2227
|
}
|
|
@@ -1813,7 +2273,8 @@ export class SessionRuntime {
|
|
|
1813
2273
|
reasoning: reasoningDirective(this.reasoning),
|
|
1814
2274
|
priming: transcript ? buildPriming(transcript) : undefined,
|
|
1815
2275
|
imageOutput: this.cfg.sendAgentImages ? IMAGE_OUTPUT_DIRECTIVE : undefined,
|
|
1816
|
-
progress:
|
|
2276
|
+
progress:
|
|
2277
|
+
!this.managerMode && this.cfg.showProgress ? PROGRESS_DIRECTIVE : undefined,
|
|
1817
2278
|
});
|
|
1818
2279
|
return this.runPromptWithRetries(content);
|
|
1819
2280
|
}
|
|
@@ -1860,7 +2321,8 @@ export class SessionRuntime {
|
|
|
1860
2321
|
reasoning: reasoningDirective(this.reasoning),
|
|
1861
2322
|
priming: transcript ? buildPriming(transcript) : undefined,
|
|
1862
2323
|
imageOutput: this.cfg.sendAgentImages ? IMAGE_OUTPUT_DIRECTIVE : undefined,
|
|
1863
|
-
progress:
|
|
2324
|
+
progress:
|
|
2325
|
+
!this.managerMode && this.cfg.showProgress ? PROGRESS_DIRECTIVE : undefined,
|
|
1864
2326
|
});
|
|
1865
2327
|
log.info(
|
|
1866
2328
|
`chat ${this.chatId} auto-rotating to account ${t.label}` +
|
|
@@ -1990,7 +2452,8 @@ export class SessionRuntime {
|
|
|
1990
2452
|
const resumeContent = buildContentBlocks(textPrompt(RESUME_INSTRUCTION), {
|
|
1991
2453
|
reasoning: reasoningDirective(this.reasoning),
|
|
1992
2454
|
imageOutput: this.cfg.sendAgentImages ? IMAGE_OUTPUT_DIRECTIVE : undefined,
|
|
1993
|
-
progress:
|
|
2455
|
+
progress:
|
|
2456
|
+
!this.managerMode && this.cfg.showProgress ? PROGRESS_DIRECTIVE : undefined,
|
|
1994
2457
|
});
|
|
1995
2458
|
|
|
1996
2459
|
let last = final;
|
|
@@ -2061,6 +2524,111 @@ export class SessionRuntime {
|
|
|
2061
2524
|
return `\u{1F4E8} From other session ${this.sessionTag()}\n${head}\n${summarizeFileOpsShort(this.fileOps)}\n\n${tags}`;
|
|
2062
2525
|
}
|
|
2063
2526
|
|
|
2527
|
+
/** Soft completion for General manager (no file-ops / progress spam). */
|
|
2528
|
+
private managerCompletionMessage(
|
|
2529
|
+
stopReason: string | undefined,
|
|
2530
|
+
startedAt: number,
|
|
2531
|
+
streamedOutput: boolean,
|
|
2532
|
+
): string {
|
|
2533
|
+
const elapsed = fmtDuration(Date.now() - startedAt);
|
|
2534
|
+
if (this.cancelled || stopReason === "cancelled") {
|
|
2535
|
+
const msg = `\u23F9 Stopped \u00B7 ${elapsed}`;
|
|
2536
|
+
this.lastCompletion = msg;
|
|
2537
|
+
return streamedOutput ? "" : msg;
|
|
2538
|
+
}
|
|
2539
|
+
// When prose already streamed, no extra Done line (chat-like).
|
|
2540
|
+
if (streamedOutput) {
|
|
2541
|
+
this.lastCompletion = this.turnAssistantText.slice(0, 800) || `\u2705 Done \u00B7 ${elapsed}`;
|
|
2542
|
+
return "";
|
|
2543
|
+
}
|
|
2544
|
+
const msg = `\u2705 Done \u00B7 ${elapsed}`;
|
|
2545
|
+
this.lastCompletion = msg;
|
|
2546
|
+
return msg;
|
|
2547
|
+
}
|
|
2548
|
+
|
|
2549
|
+
/**
|
|
2550
|
+
* True when the next queued prompt is a continuation of the *same* manager
|
|
2551
|
+
* job (self-recheck / bridge results / suggestion batch), not a new user
|
|
2552
|
+
* ask or a different send_prompt dispatch.
|
|
2553
|
+
*/
|
|
2554
|
+
private queueIsSameJobContinuation(jobId: string): boolean {
|
|
2555
|
+
if (this.queue.length === 0) return false;
|
|
2556
|
+
const next = this.queue[0]!;
|
|
2557
|
+
if (next.reportBack && next.reportBack.jobId !== jobId) return false;
|
|
2558
|
+
if (next.reportBack && next.reportBack.jobId === jobId) return true;
|
|
2559
|
+
// Meta continuations omit reportBack but keep the open job.
|
|
2560
|
+
return (
|
|
2561
|
+
!!next.skipSelfRecheck ||
|
|
2562
|
+
isSelfRecheckPrompt(next.text) ||
|
|
2563
|
+
isTelegramBridgeResultsPrompt(next.text) ||
|
|
2564
|
+
isManagerWorkReportPrompt(next.text)
|
|
2565
|
+
);
|
|
2566
|
+
}
|
|
2567
|
+
|
|
2568
|
+
/**
|
|
2569
|
+
* If this runtime was dispatched from General, wake the manager with a
|
|
2570
|
+
* structured WORK REPORT once for this job. Waits only for same-job meta
|
|
2571
|
+
* follow-ups; reports immediately when a different dispatch/user turn is next.
|
|
2572
|
+
*/
|
|
2573
|
+
private async maybeReportBackToManager(opts: {
|
|
2574
|
+
ok: boolean;
|
|
2575
|
+
cancelled: boolean;
|
|
2576
|
+
stopReason?: string;
|
|
2577
|
+
error?: string;
|
|
2578
|
+
}): Promise<void> {
|
|
2579
|
+
const meta = this.pendingReportBack;
|
|
2580
|
+
if (!meta || !this.bridge?.wakeManager) return;
|
|
2581
|
+
// Same-job recheck / bridge results / suggestions still pending — wait.
|
|
2582
|
+
if (this.queueIsSameJobContinuation(meta.jobId)) return;
|
|
2583
|
+
|
|
2584
|
+
const status = opts.cancelled ? "cancelled" : opts.ok ? "done" : "failed";
|
|
2585
|
+
const assistantSummary = (
|
|
2586
|
+
this.turnAssistantText.trim() ||
|
|
2587
|
+
this.lastCompletion ||
|
|
2588
|
+
"(no assistant text)"
|
|
2589
|
+
).replace(/\s+/g, " ").trim();
|
|
2590
|
+
const filesSummary =
|
|
2591
|
+
this.fileOps.size > 0 ? summarizeFileOpsShort(this.fileOps) : undefined;
|
|
2592
|
+
|
|
2593
|
+
updateManagerJob(meta.jobId, {
|
|
2594
|
+
status: status === "done" ? "done" : status === "cancelled" ? "cancelled" : "failed",
|
|
2595
|
+
resultSummary: assistantSummary.slice(0, 400),
|
|
2596
|
+
childSessionId: this.sessionId,
|
|
2597
|
+
});
|
|
2598
|
+
|
|
2599
|
+
const prompt = buildManagerWorkReportPrompt({
|
|
2600
|
+
jobId: meta.jobId,
|
|
2601
|
+
targetName: meta.targetName || this.projectName || basename(this.cwd),
|
|
2602
|
+
targetThreadId: this.messageThreadId ?? 0,
|
|
2603
|
+
targetPath: meta.targetPath || this.cwd,
|
|
2604
|
+
userAskPreview: meta.userAskPreview,
|
|
2605
|
+
dispatchPromptPreview: meta.dispatchPrompt,
|
|
2606
|
+
status,
|
|
2607
|
+
stopReason: opts.stopReason,
|
|
2608
|
+
error: opts.error,
|
|
2609
|
+
assistantSummary,
|
|
2610
|
+
filesSummary,
|
|
2611
|
+
childSessionId: this.sessionId,
|
|
2612
|
+
});
|
|
2613
|
+
|
|
2614
|
+
// Clear before await so a re-entry cannot double-report this job.
|
|
2615
|
+
this.pendingReportBack = undefined;
|
|
2616
|
+
try {
|
|
2617
|
+
await this.bridge.wakeManager({
|
|
2618
|
+
originChatId: meta.originChatId,
|
|
2619
|
+
originThreadId: meta.originThreadId,
|
|
2620
|
+
prompt,
|
|
2621
|
+
});
|
|
2622
|
+
log.info(
|
|
2623
|
+
`report-back job ${meta.jobId} → general (#${meta.originThreadId}) status=${status}`,
|
|
2624
|
+
);
|
|
2625
|
+
} catch (e) {
|
|
2626
|
+
log.warn(`report-back failed for job ${meta.jobId}: ${(e as Error).message}`);
|
|
2627
|
+
// Restore so a later turn might retry once if still attached.
|
|
2628
|
+
this.pendingReportBack = meta;
|
|
2629
|
+
}
|
|
2630
|
+
}
|
|
2631
|
+
|
|
2064
2632
|
/**
|
|
2065
2633
|
* Final Done after a self-recheck: head + split file lists (first turn vs recheck).
|
|
2066
2634
|
*/
|
|
@@ -2129,6 +2697,9 @@ export class SessionRuntime {
|
|
|
2129
2697
|
/** Searchable Telegram hashtags so you can pull up every message of a session
|
|
2130
2698
|
* or project (and this turn's prompt) by tapping the tag. */
|
|
2131
2699
|
private hashtags(): string {
|
|
2700
|
+
// General manager: no tags (user requested clean chat). Reply routing uses
|
|
2701
|
+
// Telegram message-id → session map, not #sess_ footers.
|
|
2702
|
+
if (this.managerMode) return "";
|
|
2132
2703
|
return sessionHashtags({
|
|
2133
2704
|
projectName: this.projectName,
|
|
2134
2705
|
cwd: this.cwd,
|
|
@@ -2314,13 +2885,17 @@ export class SessionRuntime {
|
|
|
2314
2885
|
): Promise<number | undefined> {
|
|
2315
2886
|
const send = async (body: string, withMarkup: boolean): Promise<number | undefined> => {
|
|
2316
2887
|
try {
|
|
2317
|
-
const extra: Record<string, unknown> =
|
|
2318
|
-
|
|
2888
|
+
const extra: Record<string, unknown> = {
|
|
2889
|
+
...(opts?.loud ? { disable_notification: false } : {}),
|
|
2890
|
+
// Never pass message_thread_id=1 (General) — Telegram rejects it.
|
|
2891
|
+
...outboundThreadExtra(this.messageThreadId),
|
|
2892
|
+
};
|
|
2319
2893
|
if (opts?.replyTo !== undefined) {
|
|
2320
2894
|
extra.reply_parameters = { message_id: opts.replyTo, allow_sending_without_reply: true };
|
|
2321
2895
|
}
|
|
2322
2896
|
if (withMarkup && opts?.replyMarkup) extra.reply_markup = opts.replyMarkup;
|
|
2323
2897
|
const msg = await this.api.sendMessage(this.chatId, body, extra);
|
|
2898
|
+
if (this.sessionId) this.onTelegramMessageBound?.(msg.message_id, this.sessionId);
|
|
2324
2899
|
return msg.message_id;
|
|
2325
2900
|
} catch (e) {
|
|
2326
2901
|
log.debug("notify failed:", (e as Error).message);
|
|
@@ -2344,8 +2919,8 @@ export class SessionRuntime {
|
|
|
2344
2919
|
): Promise<void> {
|
|
2345
2920
|
if (messageId !== undefined) {
|
|
2346
2921
|
try {
|
|
2922
|
+
// editMessageText does not need message_thread_id; keep markup only.
|
|
2347
2923
|
const extra: Record<string, unknown> = {};
|
|
2348
|
-
if (this.messageThreadId !== undefined) extra.message_thread_id = this.messageThreadId;
|
|
2349
2924
|
if (markup) extra.reply_markup = markup;
|
|
2350
2925
|
await this.api.editMessageText(this.chatId, messageId, text, extra);
|
|
2351
2926
|
return;
|
|
@@ -2378,6 +2953,41 @@ export class SessionRuntime {
|
|
|
2378
2953
|
}
|
|
2379
2954
|
}
|
|
2380
2955
|
|
|
2956
|
+
/** Visible manager reply body (no progress markers / telegram action fences). */
|
|
2957
|
+
function cleanManagerVisibleText(raw: string): string {
|
|
2958
|
+
if (!raw?.trim()) return "";
|
|
2959
|
+
const withoutTg = stripTelegramActionFences(raw);
|
|
2960
|
+
return extractProgress(withoutTg).cleaned.trim();
|
|
2961
|
+
}
|
|
2962
|
+
|
|
2963
|
+
/**
|
|
2964
|
+
* One short user-facing fallback when General forgot `notify`.
|
|
2965
|
+
* Drops empty, table-spam, and pure status narration.
|
|
2966
|
+
*/
|
|
2967
|
+
export function pickManagerFallbackText(cleaned: string): string | undefined {
|
|
2968
|
+
let t = cleaned.replace(/\r\n/g, "\n").trim();
|
|
2969
|
+
if (!t) return undefined;
|
|
2970
|
+
// Drop markdown tables and multi-line job dumps.
|
|
2971
|
+
if (/^\s*\|.+\|/m.test(t) && (t.match(/\|/g) || []).length >= 6) return undefined;
|
|
2972
|
+
// Collapse whitespace.
|
|
2973
|
+
t = t.replace(/\n{3,}/g, "\n\n").trim();
|
|
2974
|
+
// Prefer first 1–2 short paragraphs.
|
|
2975
|
+
const paras = t.split(/\n\n+/).map((p) => p.trim()).filter(Boolean);
|
|
2976
|
+
let out = paras.slice(0, 2).join("\n\n");
|
|
2977
|
+
if (out.length > 600) out = out.slice(0, 597) + "\u2026";
|
|
2978
|
+
// Ignore pure meta / empty placeholders.
|
|
2979
|
+
if (/^(thinking|starting|ok|done|\.+|\u2026)+$/i.test(out.trim())) return undefined;
|
|
2980
|
+
if (out.length < 2) return undefined;
|
|
2981
|
+
// Skip "Dispatching… / Sending to…" spam patterns if that is all we got.
|
|
2982
|
+
if (
|
|
2983
|
+
/^(dispatching|sending to|queued|cancelling|already running)\b/i.test(out) &&
|
|
2984
|
+
out.length < 280
|
|
2985
|
+
) {
|
|
2986
|
+
return undefined;
|
|
2987
|
+
}
|
|
2988
|
+
return out;
|
|
2989
|
+
}
|
|
2990
|
+
|
|
2381
2991
|
/** Format an elapsed duration compactly (e.g. "8s", "2m 13s", "1h 4m"). */
|
|
2382
2992
|
function fmtDuration(ms: number): string {
|
|
2383
2993
|
const s = Math.round(ms / 1000);
|