grok-telegram-bot 2.5.0 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +71 -0
- package/README.md +5 -3
- package/docs/GROUP.md +39 -4
- package/package.json +4 -4
- package/src/app/types.ts +27 -1
- package/src/bot/auth.ts +5 -1
- package/src/bot/bot.ts +73 -4
- package/src/bot/chat-controller.ts +129 -0
- package/src/bot/group-memory.ts +192 -12
- package/src/bot/handlers/forum.ts +16 -6
- package/src/bot/handlers/message.ts +165 -25
- package/src/bot/handlers/photo.ts +4 -1
- package/src/bot/image-return.ts +4 -1
- package/src/bot/manager-context.ts +208 -0
- package/src/bot/manager-jobs.ts +142 -0
- package/src/bot/menu/ephemeral.ts +4 -1
- package/src/bot/prompt-anchor.ts +2 -3
- package/src/bot/prompt-content.ts +5 -0
- package/src/bot/scope.ts +9 -8
- package/src/bot/session-runtime.ts +663 -55
- package/src/bot/telegram-actions.ts +728 -38
- package/src/bot/telegram-bots.ts +2 -1
- package/src/bot/telegram-io.ts +4 -1
- package/src/forum/manager.ts +2 -1
- package/src/forum/thread.ts +33 -0
- package/src/render/manager-directive.ts +137 -0
- package/src/render/session-comment.ts +10 -0
- package/src/render/telegram-bridge.ts +118 -14
- package/src/sessions/history.ts +18 -0
- package/src/stream/streamer.ts +46 -10
|
@@ -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`);
|
|
@@ -756,7 +847,8 @@ export class SessionRuntime {
|
|
|
756
847
|
if (
|
|
757
848
|
input.skipSelfRecheck ||
|
|
758
849
|
isSelfRecheckPrompt(input.text) ||
|
|
759
|
-
isTelegramBridgeResultsPrompt(input.text)
|
|
850
|
+
isTelegramBridgeResultsPrompt(input.text) ||
|
|
851
|
+
isManagerWorkReportPrompt(input.text)
|
|
760
852
|
) {
|
|
761
853
|
return false;
|
|
762
854
|
}
|
|
@@ -774,6 +866,30 @@ export class SessionRuntime {
|
|
|
774
866
|
return true;
|
|
775
867
|
}
|
|
776
868
|
|
|
869
|
+
/** Memory + topic catalog inject for every real manager user turn. */
|
|
870
|
+
private applyManagerContext(input: PromptInput): PromptInput {
|
|
871
|
+
if (!this.managerMode) return input;
|
|
872
|
+
if (
|
|
873
|
+
isTelegramBridgeResultsPrompt(input.text) ||
|
|
874
|
+
isManagerWorkReportPrompt(input.text) ||
|
|
875
|
+
isSelfRecheckPrompt(input.text)
|
|
876
|
+
) {
|
|
877
|
+
return input;
|
|
878
|
+
}
|
|
879
|
+
if (!this.bridge) return input;
|
|
880
|
+
const userText = stripDirectiveWrappers(input.text) || input.text;
|
|
881
|
+
const block = buildManagerContextBlock({
|
|
882
|
+
userText,
|
|
883
|
+
sessionsDir: this.cfg.sessionsDir,
|
|
884
|
+
store: this.bridge.store,
|
|
885
|
+
forum: this.bridge.forum,
|
|
886
|
+
});
|
|
887
|
+
return {
|
|
888
|
+
...input,
|
|
889
|
+
text: injectManagerContext(input.text, block),
|
|
890
|
+
};
|
|
891
|
+
}
|
|
892
|
+
|
|
777
893
|
/**
|
|
778
894
|
* Stop the current turn for this runtime only.
|
|
779
895
|
* Soft ACP cancel + session-scoped force-complete; never kills the shared
|
|
@@ -903,7 +1019,12 @@ export class SessionRuntime {
|
|
|
903
1019
|
private async runTurn(input: PromptInput): Promise<void> {
|
|
904
1020
|
// Apply before any turn bookkeeping so card previews / logs see the wrapped
|
|
905
1021
|
// text the same way the agent does (also covers flushQueue first messages).
|
|
906
|
-
|
|
1022
|
+
try {
|
|
1023
|
+
input = this.applyFirstPromptSteering(input);
|
|
1024
|
+
input = this.applyManagerContext(input);
|
|
1025
|
+
} catch (e) {
|
|
1026
|
+
log.warn(`prompt steering/context failed: ${(e as Error).message}`);
|
|
1027
|
+
}
|
|
907
1028
|
|
|
908
1029
|
this.busy = true;
|
|
909
1030
|
this.cancelled = false;
|
|
@@ -915,19 +1036,29 @@ export class SessionRuntime {
|
|
|
915
1036
|
this.turnExpectDone = false;
|
|
916
1037
|
this.turnDonePinged = false;
|
|
917
1038
|
this.isSelfRecheckTurn = isSelfRecheckPrompt(input.text);
|
|
918
|
-
// Meta turns (recheck, bridge results,
|
|
1039
|
+
// Meta turns (recheck, bridge results, work reports) never arm another recheck.
|
|
919
1040
|
const isBridgeResults = isTelegramBridgeResultsPrompt(input.text);
|
|
1041
|
+
const isWorkReport = isManagerWorkReportPrompt(input.text);
|
|
1042
|
+
// Keep the Thinking… bubble across search_memory → results follow-ups so
|
|
1043
|
+
// the user is not left with a deleted placeholder and no reply.
|
|
1044
|
+
if (this.managerStatusMsgId !== undefined && !isBridgeResults) {
|
|
1045
|
+
const orphan = this.managerStatusMsgId;
|
|
1046
|
+
this.managerStatusMsgId = undefined;
|
|
1047
|
+
void this.deleteManagerStatus(orphan);
|
|
1048
|
+
}
|
|
920
1049
|
this.skipSelfRecheck =
|
|
921
1050
|
!!input.skipSelfRecheck ||
|
|
922
1051
|
this.isSelfRecheckTurn ||
|
|
923
|
-
isBridgeResults
|
|
1052
|
+
isBridgeResults ||
|
|
1053
|
+
isWorkReport ||
|
|
1054
|
+
this.managerMode;
|
|
924
1055
|
// Fresh user work resets bridge-chain depth + suggestion anchors.
|
|
925
|
-
if (!this.isSelfRecheckTurn && !isBridgeResults) {
|
|
1056
|
+
if (!this.isSelfRecheckTurn && !isBridgeResults && !isWorkReport) {
|
|
926
1057
|
this.bridgeResultDepth = 0;
|
|
927
1058
|
}
|
|
928
1059
|
// Fresh user work resets suggestion anchors; recheck / bridge results keep the original ask.
|
|
929
1060
|
// Strip complexity/reply wrappers so suggestions + recheck see the real ask.
|
|
930
|
-
if (!this.isSelfRecheckTurn && !isBridgeResults) {
|
|
1061
|
+
if (!this.isSelfRecheckTurn && !isBridgeResults && !isWorkReport) {
|
|
931
1062
|
this.suggestionUserText = stripDirectiveWrappers(input.text) || input.text;
|
|
932
1063
|
this.preRecheckAssistantText = "";
|
|
933
1064
|
this.preRecheckFileOps = new Map();
|
|
@@ -942,6 +1073,15 @@ export class SessionRuntime {
|
|
|
942
1073
|
this.setSessionComment(preview);
|
|
943
1074
|
}
|
|
944
1075
|
}
|
|
1076
|
+
// Bind THIS prompt's report-back (manager dispatch). Meta follow-ups
|
|
1077
|
+
// (recheck / bridge results) omit reportBack and keep the prior job until
|
|
1078
|
+
// the queue drains and we report once.
|
|
1079
|
+
if (input.reportBack) {
|
|
1080
|
+
this.pendingReportBack = input.reportBack as ReportBackMeta;
|
|
1081
|
+
}
|
|
1082
|
+
if (this.pendingReportBack && this.sessionId) {
|
|
1083
|
+
bindJobSession(this.pendingReportBack.jobId, this.sessionId);
|
|
1084
|
+
}
|
|
945
1085
|
this.shownToolIds = new Set();
|
|
946
1086
|
this.toolCallCache = new Map();
|
|
947
1087
|
this.fileOps = new Map();
|
|
@@ -961,9 +1101,33 @@ export class SessionRuntime {
|
|
|
961
1101
|
// A new streamed turn supersedes any transient "follow" watch of this same
|
|
962
1102
|
// session's previous in-flight turn (avoids duplicated output).
|
|
963
1103
|
if (this.watchIsFollow) this.stopWatch();
|
|
964
|
-
|
|
1104
|
+
// Manager (General): stream short chat prose (no tools/progress). Work
|
|
1105
|
+
// reports stay quiet. Bridge-result follow-ups ARE user-facing — that is
|
|
1106
|
+
// usually when the manager answers after search_memory.
|
|
1107
|
+
const managerSilent =
|
|
1108
|
+
this.managerMode && (isWorkReport || this.isSelfRecheckTurn);
|
|
1109
|
+
const live = this.foreground && !managerSilent;
|
|
965
1110
|
const startedAt = Date.now();
|
|
966
1111
|
this.turnStartedAt = startedAt;
|
|
1112
|
+
this.managerNotifyCount = 0;
|
|
1113
|
+
this.managerUserVisible = false;
|
|
1114
|
+
// General: Starting… (from message handler) → Thinking… then stream into it.
|
|
1115
|
+
// Work-report wakes stay fully silent (no bubble).
|
|
1116
|
+
const managerMeta = managerSilent;
|
|
1117
|
+
let thinkingMsgId: number | undefined = input.seedMessageId ?? this.managerStatusMsgId;
|
|
1118
|
+
if (this.managerMode && !managerSilent && this.turnReplyTo !== undefined) {
|
|
1119
|
+
if (thinkingMsgId !== undefined) {
|
|
1120
|
+
await this.editManagerStatus(thinkingMsgId, "Thinking\u2026");
|
|
1121
|
+
} else {
|
|
1122
|
+
thinkingMsgId = await this.postManagerStatus(this.turnReplyTo, "Thinking\u2026");
|
|
1123
|
+
}
|
|
1124
|
+
this.managerStatusMsgId = thinkingMsgId;
|
|
1125
|
+
} else if (this.managerMode && managerSilent && thinkingMsgId !== undefined && !isBridgeResults) {
|
|
1126
|
+
// Drop Starting… leftover on silent work-report / recheck turns.
|
|
1127
|
+
await this.deleteManagerStatus(thinkingMsgId);
|
|
1128
|
+
thinkingMsgId = undefined;
|
|
1129
|
+
this.managerStatusMsgId = undefined;
|
|
1130
|
+
}
|
|
967
1131
|
this.streamer = live
|
|
968
1132
|
? new ResponseStreamer(
|
|
969
1133
|
this.api,
|
|
@@ -971,13 +1135,30 @@ export class SessionRuntime {
|
|
|
971
1135
|
this.cfg.streamThrottleMs,
|
|
972
1136
|
this.turnReplyTo,
|
|
973
1137
|
this.hashtags(),
|
|
974
|
-
(pct) => this.setProgress(pct),
|
|
975
|
-
this.cfg.progressFallback,
|
|
1138
|
+
this.managerMode ? undefined : (pct) => this.setProgress(pct),
|
|
1139
|
+
this.managerMode ? false : this.cfg.progressFallback,
|
|
976
1140
|
startedAt,
|
|
977
1141
|
this.messageThreadId,
|
|
1142
|
+
this.managerMode
|
|
1143
|
+
? {
|
|
1144
|
+
proseOnly: true,
|
|
1145
|
+
showProgressBar: false,
|
|
1146
|
+
seedMessageId: thinkingMsgId,
|
|
1147
|
+
}
|
|
1148
|
+
: undefined,
|
|
978
1149
|
)
|
|
979
1150
|
: undefined;
|
|
980
|
-
|
|
1151
|
+
// Bind user message (+ status bubble) → session for reply routing.
|
|
1152
|
+
if (this.managerMode && this.sessionId) {
|
|
1153
|
+
if (this.turnReplyTo !== undefined) {
|
|
1154
|
+
this.onTelegramMessageBound?.(this.turnReplyTo, this.sessionId);
|
|
1155
|
+
}
|
|
1156
|
+
if (thinkingMsgId !== undefined) {
|
|
1157
|
+
this.onTelegramMessageBound?.(thinkingMsgId, this.sessionId);
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
// Typing indicator for user-facing manager turns and live project streams.
|
|
1161
|
+
if (live || (this.managerMode && !managerMeta)) this.typing.start();
|
|
981
1162
|
this.activity(true);
|
|
982
1163
|
this.changed();
|
|
983
1164
|
this.imageScanText = "";
|
|
@@ -986,8 +1167,13 @@ export class SessionRuntime {
|
|
|
986
1167
|
const content = buildContentBlocks(input, {
|
|
987
1168
|
reasoning: reasoningDirective(this.reasoning),
|
|
988
1169
|
priming: this.primingContext,
|
|
989
|
-
imageOutput:
|
|
990
|
-
|
|
1170
|
+
imageOutput:
|
|
1171
|
+
!this.managerMode && this.cfg.sendAgentImages
|
|
1172
|
+
? IMAGE_OUTPUT_DIRECTIVE
|
|
1173
|
+
: undefined,
|
|
1174
|
+
// Manager chat: no progress spam; project topics keep the usual directive.
|
|
1175
|
+
progress:
|
|
1176
|
+
!this.managerMode && this.cfg.showProgress ? PROGRESS_DIRECTIVE : undefined,
|
|
991
1177
|
});
|
|
992
1178
|
this.primingContext = undefined;
|
|
993
1179
|
|
|
@@ -1054,8 +1240,8 @@ export class SessionRuntime {
|
|
|
1054
1240
|
// spam to the chat (live step / status panel only).
|
|
1055
1241
|
const pingDone =
|
|
1056
1242
|
canPing && (this.foreground || !hasQueued) && !queuedBridgeResults;
|
|
1057
|
-
//
|
|
1058
|
-
this.turnExpectDone = pingDone;
|
|
1243
|
+
// Manager uses notify/finishManagerUserFacing — never arm Done safety-net spam.
|
|
1244
|
+
this.turnExpectDone = pingDone && !this.managerMode;
|
|
1059
1245
|
|
|
1060
1246
|
// One-shot self-recheck: only after a real *user* turn (not meta/auto),
|
|
1061
1247
|
// with idle queue. skipSelfRecheck blocks loops after recheck / auto-batch.
|
|
@@ -1120,16 +1306,47 @@ export class SessionRuntime {
|
|
|
1120
1306
|
}
|
|
1121
1307
|
}
|
|
1122
1308
|
|
|
1309
|
+
// Manager: keep Thinking… while bridge results are chaining so the
|
|
1310
|
+
// follow-up turn can stream the real answer into the same bubble.
|
|
1311
|
+
if (this.managerMode && queuedBridgeResults && this.managerStatusMsgId !== undefined) {
|
|
1312
|
+
void this.editManagerStatus(this.managerStatusMsgId, "Thinking\u2026");
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1123
1315
|
if (!queuedRecheck && !queuedBridgeResults) {
|
|
1316
|
+
// Manager (General): quiet-by-default completion — no Done spam.
|
|
1317
|
+
// User-facing only via notify (already sent) or one fallback reply.
|
|
1318
|
+
if (this.managerMode) {
|
|
1319
|
+
await this.finishManagerUserFacing(managerMeta, final.result?.stopReason, startedAt);
|
|
1320
|
+
this.turnDonePinged = true;
|
|
1321
|
+
// Suggestions only when something was actually shown to the user.
|
|
1322
|
+
if (
|
|
1323
|
+
final.result &&
|
|
1324
|
+
!this.cancelled &&
|
|
1325
|
+
!hasQueued &&
|
|
1326
|
+
this.managerUserVisible &&
|
|
1327
|
+
!managerMeta
|
|
1328
|
+
) {
|
|
1329
|
+
try {
|
|
1330
|
+
const baseForSug =
|
|
1331
|
+
cleanManagerVisibleText(this.turnAssistantText).slice(0, 500) || "Done";
|
|
1332
|
+
await this.collectAndApplySuggestions(baseForSug, undefined, {
|
|
1333
|
+
autoQueue: true,
|
|
1334
|
+
});
|
|
1335
|
+
} catch (e) {
|
|
1336
|
+
log.debug(`manager suggestions failed: ${(e as Error).message}`);
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
} else {
|
|
1124
1340
|
// Build Done text *now* (after quiet decision) so a cancel during the
|
|
1125
1341
|
// recheck-decision wait shows ⏹ Stopped, not a stale ✅ Done head.
|
|
1126
1342
|
let doneText = this.isSelfRecheckTurn
|
|
1127
|
-
|
|
1128
|
-
|
|
1343
|
+
? this.completionMessageSplit(final.result?.stopReason, startedAt, streamedOutput)
|
|
1344
|
+
: this.completionMessage(final.result?.stopReason, startedAt, streamedOutput);
|
|
1129
1345
|
// 1) Always send Done FIRST — never block the completion ping on the
|
|
1130
1346
|
// quiet suggestions prompt (which can hang and look like "no Done").
|
|
1131
1347
|
let doneMsgId: number | undefined;
|
|
1132
|
-
|
|
1348
|
+
const shouldPingDone = pingDone;
|
|
1349
|
+
if (shouldPingDone && doneText.trim()) {
|
|
1133
1350
|
doneMsgId = await this.notify(doneText, {
|
|
1134
1351
|
loud: true,
|
|
1135
1352
|
replyTo: this.turnReplyTo,
|
|
@@ -1137,30 +1354,51 @@ export class SessionRuntime {
|
|
|
1137
1354
|
});
|
|
1138
1355
|
if (doneMsgId !== undefined) this.turnDonePinged = true;
|
|
1139
1356
|
}
|
|
1140
|
-
// 2) Suggestions
|
|
1357
|
+
// 2) Suggestions: project topics keep Done-edit UX.
|
|
1141
1358
|
if (final.result && !this.cancelled && !hasQueued) {
|
|
1142
1359
|
try {
|
|
1143
|
-
const
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1360
|
+
const baseForSug = doneText;
|
|
1361
|
+
const sug = await this.collectAndApplySuggestions(
|
|
1362
|
+
baseForSug,
|
|
1363
|
+
switchKb,
|
|
1364
|
+
{
|
|
1365
|
+
autoQueue: this.foreground,
|
|
1366
|
+
},
|
|
1367
|
+
);
|
|
1368
|
+
if (shouldPingDone && sug.text !== doneText) {
|
|
1150
1369
|
await this.enhanceDoneMessage(doneMsgId, sug.text, sug.markup ?? switchKb);
|
|
1151
1370
|
}
|
|
1152
1371
|
} catch (e) {
|
|
1153
1372
|
log.debug(`suggestions after Done failed: ${(e as Error).message}`);
|
|
1154
1373
|
}
|
|
1155
1374
|
}
|
|
1375
|
+
}
|
|
1156
1376
|
// Clear frozen first-turn ops after final Done (recheck path done).
|
|
1157
1377
|
if (this.isSelfRecheckTurn) this.preRecheckFileOps = new Map();
|
|
1378
|
+
|
|
1379
|
+
// Child work dispatched from General → wake manager with a report.
|
|
1380
|
+
// (Waits only for same-job meta follow-ups; see maybeReportBackToManager.)
|
|
1381
|
+
await this.maybeReportBackToManager({
|
|
1382
|
+
ok: !!final.result && !this.cancelled,
|
|
1383
|
+
cancelled: this.cancelled,
|
|
1384
|
+
stopReason: final.result?.stopReason,
|
|
1385
|
+
error: undefined,
|
|
1386
|
+
});
|
|
1158
1387
|
}
|
|
1159
1388
|
// queuedBridgeResults: stay quiet in chat — agent gets results via queue.
|
|
1160
1389
|
} else if (final.error) {
|
|
1161
|
-
//
|
|
1162
|
-
|
|
1163
|
-
|
|
1390
|
+
// Manager: one short important message (or edit Thinking…); never Done spam.
|
|
1391
|
+
if (this.managerMode) {
|
|
1392
|
+
await this.finishManagerError(final.error.message, startedAt);
|
|
1393
|
+
this.turnDonePinged = true;
|
|
1394
|
+
await this.maybeReportBackToManager({
|
|
1395
|
+
ok: false,
|
|
1396
|
+
cancelled: false,
|
|
1397
|
+
error: final.error.message,
|
|
1398
|
+
});
|
|
1399
|
+
} else if (this.isSelfRecheckTurn && !hasQueued) {
|
|
1400
|
+
// If the self-recheck pass itself failed, still surface Done for the
|
|
1401
|
+
// original work (split files + suggestions) so the user is not stuck.
|
|
1164
1402
|
const switchKb = this.switchKeyboard();
|
|
1165
1403
|
const pingDone = canPing && (this.foreground || !hasQueued);
|
|
1166
1404
|
this.turnExpectDone = pingDone;
|
|
@@ -1189,6 +1427,13 @@ export class SessionRuntime {
|
|
|
1189
1427
|
}
|
|
1190
1428
|
}
|
|
1191
1429
|
this.preRecheckFileOps = new Map();
|
|
1430
|
+
// Self-recheck failed after real work — still report manager job if any.
|
|
1431
|
+
await this.maybeReportBackToManager({
|
|
1432
|
+
ok: true,
|
|
1433
|
+
cancelled: this.cancelled,
|
|
1434
|
+
stopReason: "self_recheck_failed",
|
|
1435
|
+
error: final.error.message,
|
|
1436
|
+
});
|
|
1192
1437
|
} else {
|
|
1193
1438
|
const transient = isTransientError(final.error);
|
|
1194
1439
|
const liveMsg = this.errorMessage(final.error, startedAt, final.attempts, transient);
|
|
@@ -1201,6 +1446,11 @@ export class SessionRuntime {
|
|
|
1201
1446
|
});
|
|
1202
1447
|
if (id !== undefined) this.turnDonePinged = true;
|
|
1203
1448
|
}
|
|
1449
|
+
await this.maybeReportBackToManager({
|
|
1450
|
+
ok: false,
|
|
1451
|
+
cancelled: false,
|
|
1452
|
+
error: final.error.message,
|
|
1453
|
+
});
|
|
1204
1454
|
}
|
|
1205
1455
|
}
|
|
1206
1456
|
} catch (err) {
|
|
@@ -1253,6 +1503,14 @@ export class SessionRuntime {
|
|
|
1253
1503
|
}
|
|
1254
1504
|
}
|
|
1255
1505
|
this.preRecheckFileOps = new Map();
|
|
1506
|
+
} else if (this.managerMode) {
|
|
1507
|
+
await this.finishManagerError(errMsg, startedAt);
|
|
1508
|
+
this.turnDonePinged = true;
|
|
1509
|
+
await this.maybeReportBackToManager({
|
|
1510
|
+
ok: false,
|
|
1511
|
+
cancelled: this.cancelled,
|
|
1512
|
+
error: errMsg,
|
|
1513
|
+
});
|
|
1256
1514
|
} else {
|
|
1257
1515
|
const msg = `\u274C Error after ${fmtDuration(Date.now() - startedAt)}: ${errMsg}`;
|
|
1258
1516
|
this.lastCompletion = msg;
|
|
@@ -1267,11 +1525,17 @@ export class SessionRuntime {
|
|
|
1267
1525
|
});
|
|
1268
1526
|
if (id !== undefined) this.turnDonePinged = true;
|
|
1269
1527
|
}
|
|
1528
|
+
await this.maybeReportBackToManager({
|
|
1529
|
+
ok: false,
|
|
1530
|
+
cancelled: this.cancelled,
|
|
1531
|
+
error: errMsg,
|
|
1532
|
+
});
|
|
1270
1533
|
}
|
|
1271
1534
|
} finally {
|
|
1272
1535
|
// Safety net: turn completed with an expected Done ping that never landed
|
|
1273
1536
|
// (notify failed, hung path, etc.). Never block queue flush on this.
|
|
1274
|
-
|
|
1537
|
+
// Manager never uses this path (turnExpectDone is false in manager mode).
|
|
1538
|
+
if (this.turnExpectDone && !this.turnDonePinged && !this.managerMode) {
|
|
1275
1539
|
const fallback =
|
|
1276
1540
|
this.lastCompletion?.trim() ||
|
|
1277
1541
|
`\u2705 Done \u00B7 ${fmtDuration(Date.now() - startedAt)}`;
|
|
@@ -1358,20 +1622,47 @@ export class SessionRuntime {
|
|
|
1358
1622
|
cfg: this.cfg,
|
|
1359
1623
|
chatId: this.chatId,
|
|
1360
1624
|
messageThreadId: this.messageThreadId,
|
|
1625
|
+
replyToMessageId: this.turnReplyTo,
|
|
1361
1626
|
forum: this.bridge.forum,
|
|
1362
1627
|
store: this.bridge.store,
|
|
1363
1628
|
bots: this.bridge.bots,
|
|
1364
1629
|
submitTopicPrompt: this.bridge.submitTopicPrompt,
|
|
1630
|
+
managerMode: this.managerMode,
|
|
1631
|
+
managerUserAskPreview: this.suggestionUserText || cleanUserPreview(this.turnUserText, 400),
|
|
1365
1632
|
});
|
|
1366
1633
|
|
|
1634
|
+
// Count successful notify actions. Do not delete the live stream bubble
|
|
1635
|
+
// (same id as Thinking…) — that would wipe the user's visible reply.
|
|
1636
|
+
for (const r of results) {
|
|
1637
|
+
if (r.action === "notify" && r.ok) {
|
|
1638
|
+
this.managerNotifyCount++;
|
|
1639
|
+
this.managerUserVisible = true;
|
|
1640
|
+
const streamLive = this.streamer?.liveMessageId;
|
|
1641
|
+
if (
|
|
1642
|
+
this.managerStatusMsgId !== undefined &&
|
|
1643
|
+
this.managerStatusMsgId !== streamLive &&
|
|
1644
|
+
!this.streamer?.hasOutput
|
|
1645
|
+
) {
|
|
1646
|
+
const sid = this.managerStatusMsgId;
|
|
1647
|
+
this.managerStatusMsgId = undefined;
|
|
1648
|
+
void this.deleteManagerStatus(sid);
|
|
1649
|
+
}
|
|
1650
|
+
const mid = (r.data as { messageId?: number } | undefined)?.messageId;
|
|
1651
|
+
if (mid !== undefined && this.sessionId) {
|
|
1652
|
+
this.onTelegramMessageBound?.(mid, this.sessionId);
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1367
1657
|
// Only announce durable side-effects in chat (topic create/bind/cross-prompt).
|
|
1368
|
-
//
|
|
1658
|
+
// Manager mode stays quiet — use notify for user text; no auto notes.
|
|
1659
|
+
// search_memory / list_* / bot wait status stay silent — results go to the agent.
|
|
1369
1660
|
const durableActions = new Set(["create_topic", "set_path", "send_prompt"]);
|
|
1370
1661
|
const notes = results
|
|
1371
1662
|
.filter((r) => durableActions.has(r.action) && r.userNote?.trim())
|
|
1372
1663
|
.map((r) => r.userNote!)
|
|
1373
1664
|
.filter(Boolean);
|
|
1374
|
-
if (notes.length > 0 && this.foreground) {
|
|
1665
|
+
if (notes.length > 0 && this.foreground && !this.managerMode) {
|
|
1375
1666
|
await this.notify(notes.join("\n"), {
|
|
1376
1667
|
loud: true,
|
|
1377
1668
|
replyTo: this.turnReplyTo,
|
|
@@ -1418,7 +1709,7 @@ export class SessionRuntime {
|
|
|
1418
1709
|
doneText: string,
|
|
1419
1710
|
switchKb: InlineKeyboard | undefined,
|
|
1420
1711
|
opts?: { autoQueue?: boolean },
|
|
1421
|
-
): Promise<{ text: string; markup?: InlineKeyboard }> {
|
|
1712
|
+
): Promise<{ text: string; markup?: InlineKeyboard; suggestions?: Suggestion[] }> {
|
|
1422
1713
|
if (!this.cfg.suggestionsEnabled || !this.sessionId) {
|
|
1423
1714
|
return { text: doneText, markup: switchKb };
|
|
1424
1715
|
}
|
|
@@ -1428,6 +1719,8 @@ export class SessionRuntime {
|
|
|
1428
1719
|
} catch (e) {
|
|
1429
1720
|
log.debug(`suggestions fetch failed: ${(e as Error).message}`);
|
|
1430
1721
|
}
|
|
1722
|
+
// Manager: keep 1–4 short follow-ups only.
|
|
1723
|
+
if (this.managerMode) suggestions = suggestions.slice(0, 4);
|
|
1431
1724
|
if (suggestions.length === 0) return { text: doneText, markup: switchKb };
|
|
1432
1725
|
|
|
1433
1726
|
const batchId = ++this.suggestionBatchSeq;
|
|
@@ -1445,12 +1738,13 @@ export class SessionRuntime {
|
|
|
1445
1738
|
let banner: string;
|
|
1446
1739
|
if (auto.length > 0) {
|
|
1447
1740
|
const batched = formatBatchedSuggestionsPrompt(auto);
|
|
1448
|
-
|
|
1741
|
+
// Hard, visible auto-approve block (especially for General chat).
|
|
1742
|
+
const lines = auto.map((s) => `\u2022 ${s.text}`);
|
|
1449
1743
|
const autoBlock =
|
|
1450
|
-
`\n\n\
|
|
1451
|
-
|
|
1744
|
+
`\n\n\u2705 Auto Approved:\n${lines.join("\n")}` +
|
|
1745
|
+
(this.managerMode ? "" : `\n(need \u2265 ${thr}%)`);
|
|
1452
1746
|
text += autoBlock;
|
|
1453
|
-
banner = `\
|
|
1747
|
+
banner = `\u2705 Auto Approved:\n${lines.join("\n")}`;
|
|
1454
1748
|
// Single queue entry — agent executes 1) 2) 3) in one turn.
|
|
1455
1749
|
// skipSelfRecheck: auto-follow-ups must not arm another recheck cycle.
|
|
1456
1750
|
this.queue.push(
|
|
@@ -1461,7 +1755,9 @@ export class SessionRuntime {
|
|
|
1461
1755
|
);
|
|
1462
1756
|
this.changed();
|
|
1463
1757
|
} else {
|
|
1464
|
-
text +=
|
|
1758
|
+
text += this.managerMode
|
|
1759
|
+
? "\n\nTap a suggestion to continue:"
|
|
1760
|
+
: "\n\n\u{1F4A1} Suggestions \u2014 tap one to continue:";
|
|
1465
1761
|
banner = "\u{1F4A1} Suggestions \u2014 tap one to continue:";
|
|
1466
1762
|
}
|
|
1467
1763
|
|
|
@@ -1469,8 +1765,168 @@ export class SessionRuntime {
|
|
|
1469
1765
|
// the same keyboard). Cleared when a new turn starts.
|
|
1470
1766
|
this.pendingSuggestions = { batchId, suggestions, banner };
|
|
1471
1767
|
|
|
1472
|
-
|
|
1473
|
-
|
|
1768
|
+
// Keyboard: show remaining (non-auto) suggestions; if all auto, no buttons.
|
|
1769
|
+
const remaining = suggestions.filter((s) => !auto.some((a) => a.text === s.text));
|
|
1770
|
+
const markup =
|
|
1771
|
+
remaining.length > 0
|
|
1772
|
+
? suggestionsKeyboard(batchId, remaining, switchKb)
|
|
1773
|
+
: switchKb;
|
|
1774
|
+
return { text, markup, suggestions };
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
/** Post General status bubble (Starting… / Thinking…) — streamer edits it later. */
|
|
1778
|
+
private async postManagerStatus(
|
|
1779
|
+
replyTo: number,
|
|
1780
|
+
text: string,
|
|
1781
|
+
): Promise<number | undefined> {
|
|
1782
|
+
try {
|
|
1783
|
+
const extra: Record<string, unknown> = {
|
|
1784
|
+
disable_notification: true,
|
|
1785
|
+
...outboundThreadExtra(this.messageThreadId),
|
|
1786
|
+
reply_parameters: {
|
|
1787
|
+
message_id: replyTo,
|
|
1788
|
+
allow_sending_without_reply: true,
|
|
1789
|
+
},
|
|
1790
|
+
};
|
|
1791
|
+
const msg = await this.api.sendMessage(this.chatId, text, extra);
|
|
1792
|
+
return msg.message_id;
|
|
1793
|
+
} catch (e) {
|
|
1794
|
+
log.debug(`manager status "${text}" failed: ${(e as Error).message}`);
|
|
1795
|
+
return undefined;
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
private async editManagerStatus(messageId: number, text: string): Promise<void> {
|
|
1800
|
+
try {
|
|
1801
|
+
await this.api.editMessageText(this.chatId, messageId, text);
|
|
1802
|
+
} catch (e) {
|
|
1803
|
+
log.debug(`manager status edit failed: ${(e as Error).message}`);
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
private async deleteManagerStatus(messageId: number): Promise<void> {
|
|
1808
|
+
try {
|
|
1809
|
+
await this.api.deleteMessage(this.chatId, messageId);
|
|
1810
|
+
} catch (e) {
|
|
1811
|
+
log.debug(`manager status delete failed: ${(e as Error).message}`);
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
/** Surface a short important error in General; clear Thinking… bubble. */
|
|
1816
|
+
private async finishManagerError(errorMessage: string, startedAt: number): Promise<void> {
|
|
1817
|
+
const elapsed = fmtDuration(Date.now() - startedAt);
|
|
1818
|
+
const short =
|
|
1819
|
+
errorMessage.length > 280 ? errorMessage.slice(0, 277) + "\u2026" : errorMessage;
|
|
1820
|
+
const text = `\u274C ${short} \u00B7 ${elapsed}`;
|
|
1821
|
+
this.lastCompletion = text;
|
|
1822
|
+
if (this.managerStatusMsgId !== undefined) {
|
|
1823
|
+
await this.editManagerStatus(this.managerStatusMsgId, text);
|
|
1824
|
+
this.managerUserVisible = true;
|
|
1825
|
+
if (this.sessionId) this.onTelegramMessageBound?.(this.managerStatusMsgId, this.sessionId);
|
|
1826
|
+
this.managerStatusMsgId = undefined;
|
|
1827
|
+
return;
|
|
1828
|
+
}
|
|
1829
|
+
if (this.turnReplyTo !== undefined) {
|
|
1830
|
+
const id = await this.notify(text, { loud: true, replyTo: this.turnReplyTo });
|
|
1831
|
+
if (id !== undefined) this.managerUserVisible = true;
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
/**
|
|
1836
|
+
* General quiet completion:
|
|
1837
|
+
* - if notify already sent → drop Thinking… bubble
|
|
1838
|
+
* - else if direct user ask and clean prose → one short fallback message
|
|
1839
|
+
* - else → delete Thinking… and stay silent
|
|
1840
|
+
*/
|
|
1841
|
+
private async finishManagerUserFacing(
|
|
1842
|
+
metaTurn: boolean,
|
|
1843
|
+
stopReason: string | undefined,
|
|
1844
|
+
startedAt: number,
|
|
1845
|
+
): Promise<void> {
|
|
1846
|
+
const elapsed = fmtDuration(Date.now() - startedAt);
|
|
1847
|
+
if (this.cancelled || stopReason === "cancelled") {
|
|
1848
|
+
this.lastCompletion = `\u23F9 Stopped \u00B7 ${elapsed}`;
|
|
1849
|
+
// Only surface cancel if user was waiting on a visible bubble.
|
|
1850
|
+
if (this.managerStatusMsgId !== undefined && !metaTurn) {
|
|
1851
|
+
await this.editManagerStatus(this.managerStatusMsgId, this.lastCompletion);
|
|
1852
|
+
this.managerUserVisible = true;
|
|
1853
|
+
} else if (this.managerStatusMsgId !== undefined) {
|
|
1854
|
+
await this.deleteManagerStatus(this.managerStatusMsgId);
|
|
1855
|
+
}
|
|
1856
|
+
this.managerStatusMsgId = undefined;
|
|
1857
|
+
return;
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1860
|
+
// Prose already landed in the Thinking… bubble via the streamer — keep it.
|
|
1861
|
+
if (this.streamer?.hasOutput) {
|
|
1862
|
+
this.managerUserVisible = true;
|
|
1863
|
+
this.lastCompletion =
|
|
1864
|
+
cleanManagerVisibleText(this.turnAssistantText).slice(0, 500) ||
|
|
1865
|
+
`\u2705 Done \u00B7 ${elapsed}`;
|
|
1866
|
+
this.managerStatusMsgId = undefined;
|
|
1867
|
+
return;
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1870
|
+
if (this.managerNotifyCount > 0) {
|
|
1871
|
+
this.lastCompletion =
|
|
1872
|
+
cleanManagerVisibleText(this.turnAssistantText).slice(0, 500) ||
|
|
1873
|
+
`\u2705 Done \u00B7 ${elapsed}`;
|
|
1874
|
+
if (this.managerStatusMsgId !== undefined) {
|
|
1875
|
+
await this.deleteManagerStatus(this.managerStatusMsgId);
|
|
1876
|
+
this.managerStatusMsgId = undefined;
|
|
1877
|
+
}
|
|
1878
|
+
return;
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1881
|
+
// Work-report / recheck: silent unless cancelled (handled above).
|
|
1882
|
+
if (metaTurn) {
|
|
1883
|
+
this.lastCompletion = `\u2705 Done \u00B7 ${elapsed}`;
|
|
1884
|
+
if (this.managerStatusMsgId !== undefined) {
|
|
1885
|
+
await this.deleteManagerStatus(this.managerStatusMsgId);
|
|
1886
|
+
this.managerStatusMsgId = undefined;
|
|
1887
|
+
}
|
|
1888
|
+
return;
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
// Direct user ask: single fallback if the model emitted no visible prose.
|
|
1892
|
+
const cleaned = cleanManagerVisibleText(this.turnAssistantText);
|
|
1893
|
+
const fallback = pickManagerFallbackText(cleaned);
|
|
1894
|
+
if (fallback && this.managerStatusMsgId !== undefined) {
|
|
1895
|
+
await this.editManagerStatus(this.managerStatusMsgId, fallback);
|
|
1896
|
+
this.managerUserVisible = true;
|
|
1897
|
+
this.lastCompletion = fallback.slice(0, 500);
|
|
1898
|
+
if (this.sessionId) this.onTelegramMessageBound?.(this.managerStatusMsgId, this.sessionId);
|
|
1899
|
+
this.managerStatusMsgId = undefined;
|
|
1900
|
+
return;
|
|
1901
|
+
}
|
|
1902
|
+
if (fallback && this.turnReplyTo !== undefined) {
|
|
1903
|
+
const id = await this.notify(fallback, {
|
|
1904
|
+
loud: false,
|
|
1905
|
+
replyTo: this.turnReplyTo,
|
|
1906
|
+
});
|
|
1907
|
+
if (id !== undefined) {
|
|
1908
|
+
this.managerUserVisible = true;
|
|
1909
|
+
this.lastCompletion = fallback.slice(0, 500);
|
|
1910
|
+
if (this.sessionId) this.onTelegramMessageBound?.(id, this.sessionId);
|
|
1911
|
+
}
|
|
1912
|
+
} else if (this.managerStatusMsgId !== undefined) {
|
|
1913
|
+
// Never delete the placeholder leaving the user with no reply.
|
|
1914
|
+
await this.editManagerStatus(
|
|
1915
|
+
this.managerStatusMsgId,
|
|
1916
|
+
"Working on it \u2014 I\u2019ll report back here.",
|
|
1917
|
+
);
|
|
1918
|
+
this.managerUserVisible = true;
|
|
1919
|
+
this.lastCompletion = "Working on it";
|
|
1920
|
+
if (this.sessionId) this.onTelegramMessageBound?.(this.managerStatusMsgId, this.sessionId);
|
|
1921
|
+
this.managerStatusMsgId = undefined;
|
|
1922
|
+
return;
|
|
1923
|
+
} else {
|
|
1924
|
+
this.lastCompletion = `\u2705 Done \u00B7 ${elapsed}`;
|
|
1925
|
+
}
|
|
1926
|
+
if (this.managerStatusMsgId !== undefined) {
|
|
1927
|
+
await this.deleteManagerStatus(this.managerStatusMsgId);
|
|
1928
|
+
this.managerStatusMsgId = undefined;
|
|
1929
|
+
}
|
|
1474
1930
|
}
|
|
1475
1931
|
|
|
1476
1932
|
/**
|
|
@@ -1698,7 +2154,8 @@ export class SessionRuntime {
|
|
|
1698
2154
|
reasoning: reasoningDirective(this.reasoning),
|
|
1699
2155
|
priming: this.primingContext,
|
|
1700
2156
|
imageOutput: this.cfg.sendAgentImages ? IMAGE_OUTPUT_DIRECTIVE : undefined,
|
|
1701
|
-
progress:
|
|
2157
|
+
progress:
|
|
2158
|
+
!this.managerMode && this.cfg.showProgress ? PROGRESS_DIRECTIVE : undefined,
|
|
1702
2159
|
});
|
|
1703
2160
|
this.primingContext = undefined;
|
|
1704
2161
|
log.info(
|
|
@@ -1761,7 +2218,8 @@ export class SessionRuntime {
|
|
|
1761
2218
|
reasoning: reasoningDirective(this.reasoning),
|
|
1762
2219
|
priming: transcript ? buildPriming(transcript) : undefined,
|
|
1763
2220
|
imageOutput: this.cfg.sendAgentImages ? IMAGE_OUTPUT_DIRECTIVE : undefined,
|
|
1764
|
-
progress:
|
|
2221
|
+
progress:
|
|
2222
|
+
!this.managerMode && this.cfg.showProgress ? PROGRESS_DIRECTIVE : undefined,
|
|
1765
2223
|
});
|
|
1766
2224
|
return this.runPromptWithRetries(forkContent);
|
|
1767
2225
|
}
|
|
@@ -1813,7 +2271,8 @@ export class SessionRuntime {
|
|
|
1813
2271
|
reasoning: reasoningDirective(this.reasoning),
|
|
1814
2272
|
priming: transcript ? buildPriming(transcript) : undefined,
|
|
1815
2273
|
imageOutput: this.cfg.sendAgentImages ? IMAGE_OUTPUT_DIRECTIVE : undefined,
|
|
1816
|
-
progress:
|
|
2274
|
+
progress:
|
|
2275
|
+
!this.managerMode && this.cfg.showProgress ? PROGRESS_DIRECTIVE : undefined,
|
|
1817
2276
|
});
|
|
1818
2277
|
return this.runPromptWithRetries(content);
|
|
1819
2278
|
}
|
|
@@ -1860,7 +2319,8 @@ export class SessionRuntime {
|
|
|
1860
2319
|
reasoning: reasoningDirective(this.reasoning),
|
|
1861
2320
|
priming: transcript ? buildPriming(transcript) : undefined,
|
|
1862
2321
|
imageOutput: this.cfg.sendAgentImages ? IMAGE_OUTPUT_DIRECTIVE : undefined,
|
|
1863
|
-
progress:
|
|
2322
|
+
progress:
|
|
2323
|
+
!this.managerMode && this.cfg.showProgress ? PROGRESS_DIRECTIVE : undefined,
|
|
1864
2324
|
});
|
|
1865
2325
|
log.info(
|
|
1866
2326
|
`chat ${this.chatId} auto-rotating to account ${t.label}` +
|
|
@@ -1990,7 +2450,8 @@ export class SessionRuntime {
|
|
|
1990
2450
|
const resumeContent = buildContentBlocks(textPrompt(RESUME_INSTRUCTION), {
|
|
1991
2451
|
reasoning: reasoningDirective(this.reasoning),
|
|
1992
2452
|
imageOutput: this.cfg.sendAgentImages ? IMAGE_OUTPUT_DIRECTIVE : undefined,
|
|
1993
|
-
progress:
|
|
2453
|
+
progress:
|
|
2454
|
+
!this.managerMode && this.cfg.showProgress ? PROGRESS_DIRECTIVE : undefined,
|
|
1994
2455
|
});
|
|
1995
2456
|
|
|
1996
2457
|
let last = final;
|
|
@@ -2061,6 +2522,111 @@ export class SessionRuntime {
|
|
|
2061
2522
|
return `\u{1F4E8} From other session ${this.sessionTag()}\n${head}\n${summarizeFileOpsShort(this.fileOps)}\n\n${tags}`;
|
|
2062
2523
|
}
|
|
2063
2524
|
|
|
2525
|
+
/** Soft completion for General manager (no file-ops / progress spam). */
|
|
2526
|
+
private managerCompletionMessage(
|
|
2527
|
+
stopReason: string | undefined,
|
|
2528
|
+
startedAt: number,
|
|
2529
|
+
streamedOutput: boolean,
|
|
2530
|
+
): string {
|
|
2531
|
+
const elapsed = fmtDuration(Date.now() - startedAt);
|
|
2532
|
+
if (this.cancelled || stopReason === "cancelled") {
|
|
2533
|
+
const msg = `\u23F9 Stopped \u00B7 ${elapsed}`;
|
|
2534
|
+
this.lastCompletion = msg;
|
|
2535
|
+
return streamedOutput ? "" : msg;
|
|
2536
|
+
}
|
|
2537
|
+
// When prose already streamed, no extra Done line (chat-like).
|
|
2538
|
+
if (streamedOutput) {
|
|
2539
|
+
this.lastCompletion = this.turnAssistantText.slice(0, 800) || `\u2705 Done \u00B7 ${elapsed}`;
|
|
2540
|
+
return "";
|
|
2541
|
+
}
|
|
2542
|
+
const msg = `\u2705 Done \u00B7 ${elapsed}`;
|
|
2543
|
+
this.lastCompletion = msg;
|
|
2544
|
+
return msg;
|
|
2545
|
+
}
|
|
2546
|
+
|
|
2547
|
+
/**
|
|
2548
|
+
* True when the next queued prompt is a continuation of the *same* manager
|
|
2549
|
+
* job (self-recheck / bridge results / suggestion batch), not a new user
|
|
2550
|
+
* ask or a different send_prompt dispatch.
|
|
2551
|
+
*/
|
|
2552
|
+
private queueIsSameJobContinuation(jobId: string): boolean {
|
|
2553
|
+
if (this.queue.length === 0) return false;
|
|
2554
|
+
const next = this.queue[0]!;
|
|
2555
|
+
if (next.reportBack && next.reportBack.jobId !== jobId) return false;
|
|
2556
|
+
if (next.reportBack && next.reportBack.jobId === jobId) return true;
|
|
2557
|
+
// Meta continuations omit reportBack but keep the open job.
|
|
2558
|
+
return (
|
|
2559
|
+
!!next.skipSelfRecheck ||
|
|
2560
|
+
isSelfRecheckPrompt(next.text) ||
|
|
2561
|
+
isTelegramBridgeResultsPrompt(next.text) ||
|
|
2562
|
+
isManagerWorkReportPrompt(next.text)
|
|
2563
|
+
);
|
|
2564
|
+
}
|
|
2565
|
+
|
|
2566
|
+
/**
|
|
2567
|
+
* If this runtime was dispatched from General, wake the manager with a
|
|
2568
|
+
* structured WORK REPORT once for this job. Waits only for same-job meta
|
|
2569
|
+
* follow-ups; reports immediately when a different dispatch/user turn is next.
|
|
2570
|
+
*/
|
|
2571
|
+
private async maybeReportBackToManager(opts: {
|
|
2572
|
+
ok: boolean;
|
|
2573
|
+
cancelled: boolean;
|
|
2574
|
+
stopReason?: string;
|
|
2575
|
+
error?: string;
|
|
2576
|
+
}): Promise<void> {
|
|
2577
|
+
const meta = this.pendingReportBack;
|
|
2578
|
+
if (!meta || !this.bridge?.wakeManager) return;
|
|
2579
|
+
// Same-job recheck / bridge results / suggestions still pending — wait.
|
|
2580
|
+
if (this.queueIsSameJobContinuation(meta.jobId)) return;
|
|
2581
|
+
|
|
2582
|
+
const status = opts.cancelled ? "cancelled" : opts.ok ? "done" : "failed";
|
|
2583
|
+
const assistantSummary = (
|
|
2584
|
+
this.turnAssistantText.trim() ||
|
|
2585
|
+
this.lastCompletion ||
|
|
2586
|
+
"(no assistant text)"
|
|
2587
|
+
).replace(/\s+/g, " ").trim();
|
|
2588
|
+
const filesSummary =
|
|
2589
|
+
this.fileOps.size > 0 ? summarizeFileOpsShort(this.fileOps) : undefined;
|
|
2590
|
+
|
|
2591
|
+
updateManagerJob(meta.jobId, {
|
|
2592
|
+
status: status === "done" ? "done" : status === "cancelled" ? "cancelled" : "failed",
|
|
2593
|
+
resultSummary: assistantSummary.slice(0, 400),
|
|
2594
|
+
childSessionId: this.sessionId,
|
|
2595
|
+
});
|
|
2596
|
+
|
|
2597
|
+
const prompt = buildManagerWorkReportPrompt({
|
|
2598
|
+
jobId: meta.jobId,
|
|
2599
|
+
targetName: meta.targetName || this.projectName || basename(this.cwd),
|
|
2600
|
+
targetThreadId: this.messageThreadId ?? 0,
|
|
2601
|
+
targetPath: meta.targetPath || this.cwd,
|
|
2602
|
+
userAskPreview: meta.userAskPreview,
|
|
2603
|
+
dispatchPromptPreview: meta.dispatchPrompt,
|
|
2604
|
+
status,
|
|
2605
|
+
stopReason: opts.stopReason,
|
|
2606
|
+
error: opts.error,
|
|
2607
|
+
assistantSummary,
|
|
2608
|
+
filesSummary,
|
|
2609
|
+
childSessionId: this.sessionId,
|
|
2610
|
+
});
|
|
2611
|
+
|
|
2612
|
+
// Clear before await so a re-entry cannot double-report this job.
|
|
2613
|
+
this.pendingReportBack = undefined;
|
|
2614
|
+
try {
|
|
2615
|
+
await this.bridge.wakeManager({
|
|
2616
|
+
originChatId: meta.originChatId,
|
|
2617
|
+
originThreadId: meta.originThreadId,
|
|
2618
|
+
prompt,
|
|
2619
|
+
});
|
|
2620
|
+
log.info(
|
|
2621
|
+
`report-back job ${meta.jobId} → general (#${meta.originThreadId}) status=${status}`,
|
|
2622
|
+
);
|
|
2623
|
+
} catch (e) {
|
|
2624
|
+
log.warn(`report-back failed for job ${meta.jobId}: ${(e as Error).message}`);
|
|
2625
|
+
// Restore so a later turn might retry once if still attached.
|
|
2626
|
+
this.pendingReportBack = meta;
|
|
2627
|
+
}
|
|
2628
|
+
}
|
|
2629
|
+
|
|
2064
2630
|
/**
|
|
2065
2631
|
* Final Done after a self-recheck: head + split file lists (first turn vs recheck).
|
|
2066
2632
|
*/
|
|
@@ -2129,6 +2695,9 @@ export class SessionRuntime {
|
|
|
2129
2695
|
/** Searchable Telegram hashtags so you can pull up every message of a session
|
|
2130
2696
|
* or project (and this turn's prompt) by tapping the tag. */
|
|
2131
2697
|
private hashtags(): string {
|
|
2698
|
+
// General manager: no tags (user requested clean chat). Reply routing uses
|
|
2699
|
+
// Telegram message-id → session map, not #sess_ footers.
|
|
2700
|
+
if (this.managerMode) return "";
|
|
2132
2701
|
return sessionHashtags({
|
|
2133
2702
|
projectName: this.projectName,
|
|
2134
2703
|
cwd: this.cwd,
|
|
@@ -2314,13 +2883,17 @@ export class SessionRuntime {
|
|
|
2314
2883
|
): Promise<number | undefined> {
|
|
2315
2884
|
const send = async (body: string, withMarkup: boolean): Promise<number | undefined> => {
|
|
2316
2885
|
try {
|
|
2317
|
-
const extra: Record<string, unknown> =
|
|
2318
|
-
|
|
2886
|
+
const extra: Record<string, unknown> = {
|
|
2887
|
+
...(opts?.loud ? { disable_notification: false } : {}),
|
|
2888
|
+
// Never pass message_thread_id=1 (General) — Telegram rejects it.
|
|
2889
|
+
...outboundThreadExtra(this.messageThreadId),
|
|
2890
|
+
};
|
|
2319
2891
|
if (opts?.replyTo !== undefined) {
|
|
2320
2892
|
extra.reply_parameters = { message_id: opts.replyTo, allow_sending_without_reply: true };
|
|
2321
2893
|
}
|
|
2322
2894
|
if (withMarkup && opts?.replyMarkup) extra.reply_markup = opts.replyMarkup;
|
|
2323
2895
|
const msg = await this.api.sendMessage(this.chatId, body, extra);
|
|
2896
|
+
if (this.sessionId) this.onTelegramMessageBound?.(msg.message_id, this.sessionId);
|
|
2324
2897
|
return msg.message_id;
|
|
2325
2898
|
} catch (e) {
|
|
2326
2899
|
log.debug("notify failed:", (e as Error).message);
|
|
@@ -2344,8 +2917,8 @@ export class SessionRuntime {
|
|
|
2344
2917
|
): Promise<void> {
|
|
2345
2918
|
if (messageId !== undefined) {
|
|
2346
2919
|
try {
|
|
2920
|
+
// editMessageText does not need message_thread_id; keep markup only.
|
|
2347
2921
|
const extra: Record<string, unknown> = {};
|
|
2348
|
-
if (this.messageThreadId !== undefined) extra.message_thread_id = this.messageThreadId;
|
|
2349
2922
|
if (markup) extra.reply_markup = markup;
|
|
2350
2923
|
await this.api.editMessageText(this.chatId, messageId, text, extra);
|
|
2351
2924
|
return;
|
|
@@ -2378,6 +2951,41 @@ export class SessionRuntime {
|
|
|
2378
2951
|
}
|
|
2379
2952
|
}
|
|
2380
2953
|
|
|
2954
|
+
/** Visible manager reply body (no progress markers / telegram action fences). */
|
|
2955
|
+
function cleanManagerVisibleText(raw: string): string {
|
|
2956
|
+
if (!raw?.trim()) return "";
|
|
2957
|
+
const withoutTg = stripTelegramActionFences(raw);
|
|
2958
|
+
return extractProgress(withoutTg).cleaned.trim();
|
|
2959
|
+
}
|
|
2960
|
+
|
|
2961
|
+
/**
|
|
2962
|
+
* One short user-facing fallback when General forgot `notify`.
|
|
2963
|
+
* Drops empty, table-spam, and pure status narration.
|
|
2964
|
+
*/
|
|
2965
|
+
export function pickManagerFallbackText(cleaned: string): string | undefined {
|
|
2966
|
+
let t = cleaned.replace(/\r\n/g, "\n").trim();
|
|
2967
|
+
if (!t) return undefined;
|
|
2968
|
+
// Drop markdown tables and multi-line job dumps.
|
|
2969
|
+
if (/^\s*\|.+\|/m.test(t) && (t.match(/\|/g) || []).length >= 6) return undefined;
|
|
2970
|
+
// Collapse whitespace.
|
|
2971
|
+
t = t.replace(/\n{3,}/g, "\n\n").trim();
|
|
2972
|
+
// Prefer first 1–2 short paragraphs.
|
|
2973
|
+
const paras = t.split(/\n\n+/).map((p) => p.trim()).filter(Boolean);
|
|
2974
|
+
let out = paras.slice(0, 2).join("\n\n");
|
|
2975
|
+
if (out.length > 600) out = out.slice(0, 597) + "\u2026";
|
|
2976
|
+
// Ignore pure meta / empty placeholders.
|
|
2977
|
+
if (/^(thinking|starting|ok|done|\.+|\u2026)+$/i.test(out.trim())) return undefined;
|
|
2978
|
+
if (out.length < 2) return undefined;
|
|
2979
|
+
// Skip "Dispatching… / Sending to…" spam patterns if that is all we got.
|
|
2980
|
+
if (
|
|
2981
|
+
/^(dispatching|sending to|queued|cancelling|already running)\b/i.test(out) &&
|
|
2982
|
+
out.length < 280
|
|
2983
|
+
) {
|
|
2984
|
+
return undefined;
|
|
2985
|
+
}
|
|
2986
|
+
return out;
|
|
2987
|
+
}
|
|
2988
|
+
|
|
2381
2989
|
/** Format an elapsed duration compactly (e.g. "8s", "2m 13s", "1h 4m"). */
|
|
2382
2990
|
function fmtDuration(ms: number): string {
|
|
2383
2991
|
const s = Math.round(ms / 1000);
|