grok-telegram-bot 2.3.1 → 2.4.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 +26 -0
- package/CHANGELOG.md +37 -0
- package/package.json +1 -1
- package/scripts/analyze-jsonl.ts +33 -0
- package/scripts/delayed-restart.ps1 +29 -0
- package/scripts/probe-exit-response-shape.py +77 -0
- package/scripts/probe-plan-exit.py +60 -0
- package/scripts/probe-plan-exit2.py +48 -0
- package/scripts/probe-plan-fields.py +41 -0
- package/scripts/probe-plan-fields2.py +58 -0
- package/scripts/probe-plan-response-path.py +48 -0
- package/scripts/sample-claude-tooluse.ts +21 -0
- package/scripts/sample-kiro-events.ts +31 -0
- package/scripts/smoke-exit-plan.ts +274 -0
- package/scripts/smoke-exit-shapes.ts +252 -0
- package/scripts/smoke-import.mjs +82 -0
- package/scripts/smoke-import.ts +73 -0
- package/src/app/accounts.ts +84 -0
- package/src/app/instance-lock.ts +6 -0
- package/src/app/types.ts +19 -2
- package/src/app/updater.ts +17 -6
- package/src/app/usage.ts +204 -7
- package/src/bot/account-rotator.ts +10 -0
- package/src/bot/bot.ts +36 -0
- package/src/bot/chat-controller.ts +35 -0
- package/src/bot/commands.ts +2 -0
- package/src/bot/complexity-gate.ts +69 -0
- package/src/bot/deps.ts +19 -0
- package/src/bot/handlers/accounts.ts +51 -1
- package/src/bot/handlers/import-session.ts +290 -0
- package/src/bot/handlers/menu.ts +17 -38
- package/src/bot/handlers/message.ts +1 -0
- package/src/bot/handlers/running.ts +35 -5
- package/src/bot/handlers/session-card.ts +12 -0
- package/src/bot/handlers/sessions.ts +14 -3
- package/src/bot/handlers/usage.ts +118 -16
- package/src/bot/menu/keyboard.ts +5 -4
- package/src/bot/menu/status-panel.ts +19 -6
- package/src/bot/prompt-content.ts +4 -0
- package/src/bot/session-fork.ts +11 -0
- package/src/bot/session-runtime.ts +740 -58
- package/src/bot/suggestions.ts +429 -0
- package/src/config.ts +41 -0
- package/src/grok/client.ts +91 -16
- package/src/grok/plan-approval.ts +72 -0
- package/src/grok/session-log.ts +16 -0
- package/src/grok/types.ts +21 -2
- package/src/import/build-import.ts +132 -0
- package/src/import/history-readers.ts +681 -0
- package/src/import/list-running.ts +100 -0
- package/src/import/sources.ts +78 -0
- package/src/index.ts +179 -24
- package/src/render/diff.ts +11 -2
- package/src/render/file-summary.ts +31 -1
- package/src/render/markdown.ts +293 -35
- package/src/render/plan.ts +127 -0
- package/src/render/session-comment.ts +261 -0
- package/src/render/tool-call-detail.ts +400 -19
- package/src/render/tool-call-merge.ts +115 -0
- package/src/render/tool-call.ts +405 -142
- package/src/render/truncate.ts +85 -0
- package/src/service/windows.ts +14 -2
- package/src/sessions/history.ts +57 -0
- package/src/sessions/store.ts +3 -0
- package/src/sessions/types.ts +5 -0
- package/src/stream/streamer.ts +73 -9
- package/src/tasks/runner.ts +4 -3
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Display-only truncation helpers.
|
|
3
|
+
*
|
|
4
|
+
* Long tool outputs, diffs, and reasoning are shortened for Telegram so messages
|
|
5
|
+
* stay readable and under size limits. Session history / agent context is
|
|
6
|
+
* unaffected — these helpers never touch what is stored on disk for context.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** Ellipsis used in truncated regions (single character). */
|
|
10
|
+
const ELLIPSIS = "\u2026";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Keep the start of `text` when longer than `max` (classic head truncate).
|
|
14
|
+
* Prefer {@link truncateMiddle} for long command/diff/reasoning bodies.
|
|
15
|
+
*/
|
|
16
|
+
export function truncateHead(text: string, max: number): string {
|
|
17
|
+
if (max <= 0) return "";
|
|
18
|
+
if (text.length <= max) return text;
|
|
19
|
+
if (max === 1) return ELLIPSIS;
|
|
20
|
+
return text.slice(0, max - 1) + ELLIPSIS;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Keep both ends of a long string, cutting the middle with an explicit note.
|
|
25
|
+
* Head ~40% / tail ~60% of the budget so recent terminal output stays visible.
|
|
26
|
+
*/
|
|
27
|
+
export function truncateMiddle(text: string, max: number): string {
|
|
28
|
+
if (max <= 0) return "";
|
|
29
|
+
if (text.length <= max) return text;
|
|
30
|
+
if (max <= 24) return truncateHead(text, max);
|
|
31
|
+
|
|
32
|
+
const omitted = text.length;
|
|
33
|
+
const marker = `\n${ELLIPSIS} (${omitted.toLocaleString("en-US")} chars total; middle omitted) ${ELLIPSIS}\n`;
|
|
34
|
+
const budget = max - marker.length;
|
|
35
|
+
if (budget < 8) return truncateHead(text, max);
|
|
36
|
+
|
|
37
|
+
const headLen = Math.max(4, Math.floor(budget * 0.4));
|
|
38
|
+
const tailLen = Math.max(4, budget - headLen);
|
|
39
|
+
// Avoid overlapping when max is only slightly under length.
|
|
40
|
+
if (headLen + tailLen >= text.length) return text;
|
|
41
|
+
return text.slice(0, headLen) + marker + text.slice(text.length - tailLen);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Middle-truncate a multi-line block by whole lines when possible (better for
|
|
46
|
+
* diffs and command output). Falls back to character middle-truncate.
|
|
47
|
+
*/
|
|
48
|
+
export function truncateMiddleLines(text: string, maxLines: number, maxChars = 12_000): string {
|
|
49
|
+
const lines = text.split("\n");
|
|
50
|
+
let body: string;
|
|
51
|
+
if (lines.length <= maxLines) {
|
|
52
|
+
body = text;
|
|
53
|
+
} else {
|
|
54
|
+
const headN = Math.max(1, Math.floor(maxLines * 0.45));
|
|
55
|
+
const tailN = Math.max(1, maxLines - headN);
|
|
56
|
+
const omitted = lines.length - headN - tailN;
|
|
57
|
+
body = [
|
|
58
|
+
...lines.slice(0, headN),
|
|
59
|
+
`${ELLIPSIS} (${omitted} lines omitted) ${ELLIPSIS}`,
|
|
60
|
+
...lines.slice(lines.length - tailN),
|
|
61
|
+
].join("\n");
|
|
62
|
+
}
|
|
63
|
+
return truncateMiddle(body, maxChars);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Live terminal display: keep the first output line + the last `tailLines`
|
|
68
|
+
* lines so long command runs update in place without spamming new blocks.
|
|
69
|
+
* Display-only — full text stays in the agent session / tool snapshot.
|
|
70
|
+
*/
|
|
71
|
+
export function formatLiveTerminalOutput(full: string, tailLines = 12, maxChars = 3500): string {
|
|
72
|
+
const normalized = full.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
73
|
+
const lines = normalized.split("\n");
|
|
74
|
+
// Drop trailing empty lines for a cleaner tail.
|
|
75
|
+
while (lines.length > 1 && lines[lines.length - 1] === "") lines.pop();
|
|
76
|
+
if (lines.length === 0) return "";
|
|
77
|
+
if (lines.length <= tailLines + 1) {
|
|
78
|
+
return truncateMiddle(lines.join("\n"), maxChars);
|
|
79
|
+
}
|
|
80
|
+
const first = lines[0]!;
|
|
81
|
+
const tail = lines.slice(-tailLines);
|
|
82
|
+
const omitted = lines.length - 1 - tailLines;
|
|
83
|
+
const body = [first, `${ELLIPSIS} (${omitted} lines omitted; live tail) ${ELLIPSIS}`, ...tail].join("\n");
|
|
84
|
+
return truncateMiddle(body, maxChars);
|
|
85
|
+
}
|
package/src/service/windows.ts
CHANGED
|
@@ -168,20 +168,32 @@ function entryOf(spec: LaunchSpec): string {
|
|
|
168
168
|
}
|
|
169
169
|
|
|
170
170
|
function vbsLauncher(spec: LaunchSpec): string {
|
|
171
|
+
// Forever-restart loop: wait for the bot process to exit, pause, relaunch.
|
|
172
|
+
// Prevents silent death when the node process crashes. GROK_TG_SUPERVISED=1
|
|
173
|
+
// tells the bot a supervisor will relaunch it (auto-update exits cleanly).
|
|
171
174
|
const cmd = `""${spec.nodePath}"" ${spec.args.map((a) => `""${a}""`).join(" ")}`;
|
|
172
175
|
return [
|
|
173
176
|
'Set sh = CreateObject("WScript.Shell")',
|
|
174
177
|
`sh.CurrentDirectory = "${spec.cwd}"`,
|
|
175
|
-
|
|
178
|
+
'Set env = sh.Environment("PROCESS")',
|
|
179
|
+
'env("GROK_TG_SUPERVISED") = "1"',
|
|
180
|
+
"Do",
|
|
181
|
+
` sh.Run "${cmd}", 0, True`,
|
|
182
|
+
" WScript.Sleep 3000",
|
|
183
|
+
"Loop",
|
|
176
184
|
].join("\r\n");
|
|
177
185
|
}
|
|
178
186
|
|
|
179
187
|
function killScript(entry: string): string {
|
|
180
188
|
const safe = entry.replace(/'/g, "''");
|
|
189
|
+
// Kill the forever-restart VBS host *before* node, otherwise the 3s loop
|
|
190
|
+
// respawns the bot right after "stop". Targets our service / Startup launchers.
|
|
181
191
|
return [
|
|
192
|
+
`$vbs = Get-CimInstance Win32_Process -Filter "Name='wscript.exe' OR Name='cscript.exe'" | Where-Object { $_.CommandLine -like '*GrokTelegramBot.vbs*' -or $_.CommandLine -like '*run-service.vbs*' };`,
|
|
193
|
+
`$vbs | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue };`,
|
|
182
194
|
`$p = Get-CimInstance Win32_Process -Filter "Name='node.exe'" | Where-Object { $_.CommandLine -like '*${safe}*' };`,
|
|
183
195
|
`$p | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue };`,
|
|
184
|
-
`"killed " + (@($p).Count)`,
|
|
196
|
+
`"killed " + (@($p).Count) + " node, " + (@($vbs).Count) + " vbs"`,
|
|
185
197
|
].join(" ");
|
|
186
198
|
}
|
|
187
199
|
|
package/src/sessions/history.ts
CHANGED
|
@@ -46,6 +46,48 @@ export function jsonlMtimeMs(jsonlPath: string): number {
|
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Best-effort card blurb from the tail of a session log: last assistant prose
|
|
51
|
+
* (what was solved), else last user prompt. Skips import-confirm noise.
|
|
52
|
+
*/
|
|
53
|
+
export function readLastCardSummary(jsonlPath: string, maxEntries = 30): string {
|
|
54
|
+
const entries = readHistory(jsonlPath, maxEntries);
|
|
55
|
+
if (entries.length === 0) return "";
|
|
56
|
+
// Walk newest → oldest for a useful assistant conclusion.
|
|
57
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
58
|
+
const e = entries[i]!;
|
|
59
|
+
if (e.role === "assistant" && e.text.trim()) {
|
|
60
|
+
const t = cleanCardProse(e.text);
|
|
61
|
+
if (t.length >= 20) return t;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
65
|
+
const e = entries[i]!;
|
|
66
|
+
if (e.role === "user" && e.text.trim()) {
|
|
67
|
+
const t = cleanCardProse(e.text);
|
|
68
|
+
if (t && !/session import complete/i.test(t)) return t;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return "";
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function cleanCardProse(raw: string, max = 200): string {
|
|
75
|
+
let t = extractProgress(raw).cleaned;
|
|
76
|
+
t = t.replace(/```[\s\S]*?```/g, " ");
|
|
77
|
+
t = t.replace(/^COMPLEXITY \(decide yourself[\s\S]*?User task:\s*/i, "");
|
|
78
|
+
t = t.replace(/^TASK COMPLEXITY:[\s\S]*?User task:\s*/i, "");
|
|
79
|
+
if (/^Session status update \(meta only\)/i.test(t.trim())) return "";
|
|
80
|
+
t = t.replace(/\s+/g, " ").trim();
|
|
81
|
+
if (t.length <= max) return t;
|
|
82
|
+
// Prefer the ending (conclusions).
|
|
83
|
+
if (t.length > max + 40) {
|
|
84
|
+
const tail = t.slice(-max + 1);
|
|
85
|
+
const sp = tail.indexOf(" ");
|
|
86
|
+
return "\u2026" + (sp > 0 && sp < 30 ? tail.slice(sp + 1) : tail);
|
|
87
|
+
}
|
|
88
|
+
return t.slice(0, max - 1) + "\u2026";
|
|
89
|
+
}
|
|
90
|
+
|
|
49
91
|
/** The first user prompt in a session log (read from the start), or "". */
|
|
50
92
|
export function readFirstPrompt(jsonlPath: string, maxBytes = 256 * 1024): string {
|
|
51
93
|
let size: number;
|
|
@@ -163,6 +205,21 @@ function cleanStoredText(text: string): string {
|
|
|
163
205
|
let t = extractProgress(text).cleaned;
|
|
164
206
|
if (t.includes(PROGRESS_DIRECTIVE)) t = t.split(PROGRESS_DIRECTIVE).join("").trim();
|
|
165
207
|
if (t.includes(IMAGE_OUTPUT_DIRECTIVE)) t = t.split(IMAGE_OUTPUT_DIRECTIVE).join("").trim();
|
|
208
|
+
// Strip first-prompt auto-complexity steering (and legacy forced-complex wrapper)
|
|
209
|
+
// so history / cards show the real user task, not bot plumbing.
|
|
210
|
+
const taskMarker = "User task:";
|
|
211
|
+
const ti = t.lastIndexOf(taskMarker);
|
|
212
|
+
if (
|
|
213
|
+
ti !== -1 &&
|
|
214
|
+
(/^COMPLEXITY \(decide yourself/i.test(t) || /^TASK COMPLEXITY:/i.test(t))
|
|
215
|
+
) {
|
|
216
|
+
t = t.slice(ti + taskMarker.length).trim();
|
|
217
|
+
}
|
|
218
|
+
// Drop removed/quiet meta-prompts if they landed in history.
|
|
219
|
+
if (/^Session status update \(meta only\)/i.test(t.trim())) t = "";
|
|
220
|
+
if (/^FOLLOW-UP SUGGESTIONS \(meta only\)/i.test(t.trim())) t = "";
|
|
221
|
+
if (/^SELF-RECHECK DECISION \(meta only\)/i.test(t.trim())) t = "";
|
|
222
|
+
if (/^SELF-RECHECK \(automatic quality pass/i.test(t.trim())) t = "";
|
|
166
223
|
return t;
|
|
167
224
|
}
|
|
168
225
|
|
package/src/sessions/store.ts
CHANGED
|
@@ -17,6 +17,7 @@ interface RawSessionJson {
|
|
|
17
17
|
created_at?: string;
|
|
18
18
|
updated_at?: string;
|
|
19
19
|
session_created_reason?: string;
|
|
20
|
+
comment?: string;
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
interface RawLock {
|
|
@@ -93,6 +94,7 @@ export class SessionStore {
|
|
|
93
94
|
/* no history yet */
|
|
94
95
|
}
|
|
95
96
|
|
|
97
|
+
const comment = (raw.comment || "").trim();
|
|
96
98
|
return {
|
|
97
99
|
sessionId,
|
|
98
100
|
cwd: raw.cwd || "",
|
|
@@ -103,6 +105,7 @@ export class SessionStore {
|
|
|
103
105
|
lockPid,
|
|
104
106
|
active,
|
|
105
107
|
historyBytes,
|
|
108
|
+
comment: comment || undefined,
|
|
106
109
|
};
|
|
107
110
|
}
|
|
108
111
|
|
package/src/sessions/types.ts
CHANGED
|
@@ -13,6 +13,11 @@ export interface SessionMeta {
|
|
|
13
13
|
active: boolean;
|
|
14
14
|
/** Size of the .jsonl history in bytes (proxy for conversation length). */
|
|
15
15
|
historyBytes: number;
|
|
16
|
+
/**
|
|
17
|
+
* Short status line for cards: current step while working, or chat summary
|
|
18
|
+
* when idle (persisted by the bot after turns).
|
|
19
|
+
*/
|
|
20
|
+
comment?: string;
|
|
16
21
|
}
|
|
17
22
|
|
|
18
23
|
export type HistoryRole = "user" | "assistant" | "tool" | "system";
|
package/src/stream/streamer.ts
CHANGED
|
@@ -15,15 +15,19 @@ import { chunkMarkdown } from "../render/chunk.js";
|
|
|
15
15
|
import { toTelegramMarkdown } from "../render/markdown.js";
|
|
16
16
|
import { extractProgress, progressBar } from "../render/progress.js";
|
|
17
17
|
import { estimateProgress } from "../render/progress-estimate.js";
|
|
18
|
+
import { truncateMiddle } from "../render/truncate.js";
|
|
18
19
|
import { safeEdit, safeSend } from "../bot/telegram-io.js";
|
|
19
20
|
|
|
20
21
|
const SOFT_LIMIT = 3500;
|
|
21
|
-
|
|
22
|
+
/** Display budget for a thinking block (middle-truncated; session context keeps all). */
|
|
23
|
+
const THINK_DISPLAY_MAX = 2800;
|
|
22
24
|
|
|
23
25
|
type SegKind = "out" | "think" | "tool";
|
|
24
26
|
interface Seg {
|
|
25
27
|
kind: SegKind;
|
|
26
28
|
text: string;
|
|
29
|
+
/** When set, later tool updates replace this segment instead of appending. */
|
|
30
|
+
toolId?: string;
|
|
27
31
|
}
|
|
28
32
|
|
|
29
33
|
export class ResponseStreamer {
|
|
@@ -44,6 +48,11 @@ export class ResponseStreamer {
|
|
|
44
48
|
private toolCalls = 0;
|
|
45
49
|
private outChars = 0;
|
|
46
50
|
private thoughtChars = 0;
|
|
51
|
+
/**
|
|
52
|
+
* Active plan board (ACP sessionUpdate "plan"). Always rendered just above
|
|
53
|
+
* the progress bar when set — done / in-progress / pending steps.
|
|
54
|
+
*/
|
|
55
|
+
private planMarkdown: string | undefined;
|
|
47
56
|
|
|
48
57
|
constructor(
|
|
49
58
|
private readonly api: Api,
|
|
@@ -134,6 +143,10 @@ export class ResponseStreamer {
|
|
|
134
143
|
this.schedule();
|
|
135
144
|
}
|
|
136
145
|
|
|
146
|
+
/**
|
|
147
|
+
* Append a one-shot tool card (no live updates). Prefer {@link upsertTool}
|
|
148
|
+
* for ACP tool calls that stream progress/output under a stable toolCallId.
|
|
149
|
+
*/
|
|
137
150
|
addTool(rawMarkdown: string): void {
|
|
138
151
|
if (!rawMarkdown) return;
|
|
139
152
|
this.toolCalls += 1;
|
|
@@ -141,6 +154,45 @@ export class ResponseStreamer {
|
|
|
141
154
|
this.schedule();
|
|
142
155
|
}
|
|
143
156
|
|
|
157
|
+
/**
|
|
158
|
+
* Insert or replace a tool card keyed by toolCallId so one command/edit stays
|
|
159
|
+
* a single Telegram block that auto-updates (no spam of new code sections).
|
|
160
|
+
* Full tool results remain in the agent session; this is display-only.
|
|
161
|
+
*/
|
|
162
|
+
/** Replace the live plan board (or clear with empty/undefined). */
|
|
163
|
+
setPlan(markdown: string | undefined): void {
|
|
164
|
+
const next = markdown?.trim() ? markdown.trim() : undefined;
|
|
165
|
+
if (next === this.planMarkdown) return;
|
|
166
|
+
this.planMarkdown = next;
|
|
167
|
+
this.schedule();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
upsertTool(toolId: string | undefined, rawMarkdown: string): void {
|
|
171
|
+
if (!rawMarkdown) return;
|
|
172
|
+
const id = (toolId || "").trim();
|
|
173
|
+
if (id) {
|
|
174
|
+
// Replace any existing segment with this id (newest first; includes rare
|
|
175
|
+
// sealed-region matches so we don't keep stale text in the segs model).
|
|
176
|
+
for (let i = this.segs.length - 1; i >= 0; i--) {
|
|
177
|
+
const s = this.segs[i]!;
|
|
178
|
+
if (s.kind === "tool" && s.toolId === id) {
|
|
179
|
+
if (s.text === rawMarkdown) return;
|
|
180
|
+
s.text = rawMarkdown;
|
|
181
|
+
// If the card lives only in a sealed bubble, also ensure a live copy
|
|
182
|
+
// so the user sees the latest output on the current message.
|
|
183
|
+
if (i < this.sealedIdx) {
|
|
184
|
+
this.segs.push({ kind: "tool", text: rawMarkdown, toolId: id });
|
|
185
|
+
}
|
|
186
|
+
this.schedule();
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
this.toolCalls += 1;
|
|
192
|
+
this.segs.push({ kind: "tool", text: rawMarkdown, toolId: id || undefined });
|
|
193
|
+
this.schedule();
|
|
194
|
+
}
|
|
195
|
+
|
|
144
196
|
get hasOutput(): boolean {
|
|
145
197
|
return this.liveId !== undefined || this.segs.some((s) => s.text.trim().length > 0);
|
|
146
198
|
}
|
|
@@ -182,13 +234,15 @@ export class ResponseStreamer {
|
|
|
182
234
|
await this.sealOverflow();
|
|
183
235
|
const base = this.captureProgress(renderSegs(this.segs.slice(this.sealedIdx)));
|
|
184
236
|
this.applyFallback();
|
|
185
|
-
// Never send an empty / progress-only bubble.
|
|
186
|
-
//
|
|
187
|
-
if (!base.trim()) return;
|
|
188
|
-
//
|
|
189
|
-
|
|
190
|
-
|
|
237
|
+
// Never send an empty / progress-only bubble. Plan alone is allowed so the
|
|
238
|
+
// board is visible as soon as the agent publishes steps.
|
|
239
|
+
if (!base.trim() && !this.planMarkdown) return;
|
|
240
|
+
// Live bubble: body → plan (always above progress) → progress bar → footer.
|
|
241
|
+
const parts: string[] = [];
|
|
242
|
+
if (base.trim()) parts.push(base);
|
|
243
|
+
if (this.planMarkdown) parts.push(this.planMarkdown);
|
|
191
244
|
if (this.progress !== undefined) parts.push(progressBar(this.progress));
|
|
245
|
+
if (parts.length === 0) return;
|
|
192
246
|
const src = `${parts.join("\n\n")}${this.footerSuffix()}`;
|
|
193
247
|
const rendered = toTelegramMarkdown(src);
|
|
194
248
|
const chunks = chunkMarkdown(rendered);
|
|
@@ -255,7 +309,17 @@ function renderSegs(segs: Seg[]): string {
|
|
|
255
309
|
function quoteThought(text: string): string {
|
|
256
310
|
const t = text.trim();
|
|
257
311
|
if (!t) return "";
|
|
258
|
-
|
|
312
|
+
// Keep both ends of long reasoning so early investigation is not lost in the UI.
|
|
313
|
+
// Truncation is display-only — the agent session retains every thought token.
|
|
314
|
+
// Neutralize fence markers and half-open emphasis so thinking never breaks
|
|
315
|
+
// MarkdownV2 parsing of the surrounding live message.
|
|
316
|
+
const safe = t
|
|
317
|
+
.replace(/```+/g, "'''")
|
|
318
|
+
.replace(/\*\*/g, "")
|
|
319
|
+
.replace(/__/g, "")
|
|
320
|
+
.replace(/~~/g, "");
|
|
321
|
+
const short = truncateMiddle(safe, THINK_DISPLAY_MAX);
|
|
259
322
|
const lines = short.split("\n");
|
|
260
|
-
|
|
323
|
+
// Plain "thinking:" (no nested *bold*) — nested markers break mid-stream.
|
|
324
|
+
return lines.map((l, i) => (i === 0 ? `> \u{1F4AD} thinking: ${l}` : `> ${l}`)).join("\n");
|
|
261
325
|
}
|
package/src/tasks/runner.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import type { Api } from "grammy";
|
|
7
7
|
import { basename } from "node:path";
|
|
8
8
|
import type { GrokClient } from "../grok/client.js";
|
|
9
|
-
import type
|
|
9
|
+
import { contentText, type SessionUpdate } from "../grok/types.js";
|
|
10
10
|
import { createLogger } from "../logger.js";
|
|
11
11
|
import { sendMarkdownDoc } from "../bot/telegram-io.js";
|
|
12
12
|
import type { Task } from "./types.js";
|
|
@@ -29,8 +29,9 @@ export class TaskRunner {
|
|
|
29
29
|
|
|
30
30
|
const listener = (sid: string, u: SessionUpdate): void => {
|
|
31
31
|
if (sid !== sessionId) return;
|
|
32
|
-
if (u.sessionUpdate === "agent_message_chunk"
|
|
33
|
-
|
|
32
|
+
if (u.sessionUpdate === "agent_message_chunk") {
|
|
33
|
+
const t = contentText(u.content);
|
|
34
|
+
if (t) text += t;
|
|
34
35
|
} else if (u.sessionUpdate === "tool_call") {
|
|
35
36
|
const id = u.toolCallId || u.title || String(tools);
|
|
36
37
|
if (!seen.has(id)) {
|