@iletai/nzb 1.6.4 → 1.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/dist/config.js CHANGED
@@ -15,6 +15,7 @@ const configSchema = z.object({
15
15
  LOG_CHANNEL_ID: z.string().optional(),
16
16
  NODE_EXTRA_CA_CERTS: z.string().optional(),
17
17
  OPENAI_API_KEY: z.string().optional(),
18
+ REASONING_EFFORT: z.string().optional(),
18
19
  });
19
20
  const raw = configSchema.parse(process.env);
20
21
  // Apply NODE_EXTRA_CA_CERTS from .env if not already set via environment.
@@ -71,6 +72,8 @@ export const config = {
71
72
  thinkingLevel: (process.env.THINKING_LEVEL || "off"),
72
73
  /** Group chat: when true, bot only responds when mentioned in groups */
73
74
  groupMentionOnly: process.env.GROUP_MENTION_ONLY !== "false",
75
+ /** Reasoning effort: low | medium | high */
76
+ reasoningEffort: (process.env.REASONING_EFFORT || "medium"),
74
77
  };
75
78
  /** Persist an env variable to ~/.nzb/.env */
76
79
  export function persistEnvVar(key, value) {
@@ -6,7 +6,6 @@ export async function getClient() {
6
6
  if (!client) {
7
7
  client = new CopilotClient({
8
8
  autoStart: true,
9
- autoRestart: true,
10
9
  });
11
10
  await client.start();
12
11
  }
@@ -204,6 +204,7 @@ async function createOrResumeSession() {
204
204
  model: config.copilotModel,
205
205
  configDir: SESSIONS_DIR,
206
206
  streaming: true,
207
+ reasoningEffort: config.reasoningEffort,
207
208
  systemMessage: {
208
209
  content: getOrchestratorSystemMessage(memorySummary || undefined, {
209
210
  selfEditEnabled: config.selfEditEnabled,
@@ -230,6 +231,7 @@ async function createOrResumeSession() {
230
231
  model: config.copilotModel,
231
232
  configDir: SESSIONS_DIR,
232
233
  streaming: true,
234
+ reasoningEffort: config.reasoningEffort,
233
235
  systemMessage: {
234
236
  content: getOrchestratorSystemMessage(memorySummary || undefined, {
235
237
  selfEditEnabled: config.selfEditEnabled,
@@ -297,7 +299,7 @@ export async function initOrchestrator(client) {
297
299
  }
298
300
  }
299
301
  /** Send a prompt on the persistent session, return the response. */
300
- async function executeOnSession(prompt, callback, onToolEvent, onUsage) {
302
+ async function executeOnSession(prompt, callback, onToolEvent, onUsage, attachments) {
301
303
  const session = await ensureOrchestratorSession();
302
304
  // Wait for any in-flight context recovery injection to finish before sending
303
305
  if (recoveryInjectionPromise) {
@@ -350,7 +352,11 @@ async function executeOnSession(prompt, callback, onToolEvent, onUsage) {
350
352
  onUsage?.({ inputTokens, outputTokens, model, duration });
351
353
  });
352
354
  try {
353
- const result = await session.sendAndWait({ prompt }, 60_000);
355
+ const sendPayload = { prompt };
356
+ if (attachments?.length) {
357
+ sendPayload.attachments = attachments;
358
+ }
359
+ const result = await session.sendAndWait(sendPayload, 60_000);
354
360
  // Allow late-arriving events (e.g. assistant.usage) to be processed
355
361
  await new Promise((r) => setTimeout(r, 150));
356
362
  const finalContent = result?.data?.content || accumulated || "(No response)";
@@ -394,7 +400,7 @@ async function processQueue() {
394
400
  const item = messageQueue.shift();
395
401
  currentSourceChannel = item.sourceChannel;
396
402
  try {
397
- const result = await executeOnSession(item.prompt, item.callback, item.onToolEvent, item.onUsage);
403
+ const result = await executeOnSession(item.prompt, item.callback, item.onToolEvent, item.onUsage, item.attachments);
398
404
  item.resolve(result);
399
405
  }
400
406
  catch (err) {
@@ -409,7 +415,7 @@ function isRecoverableError(err) {
409
415
  return /timeout|disconnect|connection|EPIPE|ECONNRESET|ECONNREFUSED|socket|closed|ENOENT|spawn|not found|expired|stale/i.test(msg);
410
416
  }
411
417
  const MAX_AUTO_CONTINUE = 3;
412
- export async function sendToOrchestrator(prompt, source, callback, onToolEvent, onUsage, _autoContinueCount = 0) {
418
+ export async function sendToOrchestrator(prompt, source, callback, onToolEvent, onUsage, _autoContinueCount = 0, attachments) {
413
419
  const sourceLabel = source.type === "telegram" ? "telegram" : source.type === "tui" ? "tui" : "background";
414
420
  logMessage("in", sourceLabel, prompt);
415
421
  // Tag the prompt with its source channel
@@ -444,6 +450,7 @@ export async function sendToOrchestrator(prompt, source, callback, onToolEvent,
444
450
  const finalContent = await new Promise((resolve, reject) => {
445
451
  const item = {
446
452
  prompt: taggedPrompt,
453
+ attachments,
447
454
  callback,
448
455
  onToolEvent,
449
456
  onUsage,
@@ -572,7 +579,7 @@ export async function resetSession() {
572
579
  // Destroy the existing session
573
580
  if (orchestratorSession) {
574
581
  try {
575
- await orchestratorSession.destroy();
582
+ await orchestratorSession.disconnect();
576
583
  }
577
584
  catch { }
578
585
  orchestratorSession = undefined;
@@ -111,7 +111,7 @@ export function createTools(deps) {
111
111
  })
112
112
  .finally(() => {
113
113
  // Auto-destroy background workers after completion to free memory (~400MB per worker)
114
- session.destroy().catch(() => { });
114
+ session.disconnect().catch(() => { });
115
115
  deps.workers.delete(args.name);
116
116
  try {
117
117
  getDb().prepare(`DELETE FROM worker_sessions WHERE name = ?`).run(args.name);
@@ -162,7 +162,7 @@ export function createTools(deps) {
162
162
  })
163
163
  .finally(() => {
164
164
  // Auto-destroy after each send_to_worker dispatch to free memory
165
- worker.session.destroy().catch(() => { });
165
+ worker.session.disconnect().catch(() => { });
166
166
  deps.workers.delete(args.name);
167
167
  try {
168
168
  getDb().prepare(`DELETE FROM worker_sessions WHERE name = ?`).run(args.name);
@@ -210,7 +210,7 @@ export function createTools(deps) {
210
210
  return `No worker named '${args.name}'.`;
211
211
  }
212
212
  try {
213
- await worker.session.destroy();
213
+ await worker.session.disconnect();
214
214
  }
215
215
  catch {
216
216
  // Session may already be gone
@@ -319,7 +319,7 @@ export function createTools(deps) {
319
319
  deps.onWorkerComplete(member.name, errMsg);
320
320
  })
321
321
  .finally(() => {
322
- session.destroy().catch(() => { });
322
+ session.disconnect().catch(() => { });
323
323
  deps.workers.delete(member.name);
324
324
  try {
325
325
  getDb().prepare(`DELETE FROM worker_sessions WHERE name = ?`).run(member.name);
@@ -341,10 +341,7 @@ export function createTools(deps) {
341
341
  defineTool("get_team_status", {
342
342
  description: "Get the status of agent teams — shows active teams, their members, and progress.",
343
343
  parameters: z.object({
344
- team_name: z
345
- .string()
346
- .optional()
347
- .describe("Specific team name to check. Omit to list all active teams."),
344
+ team_name: z.string().optional().describe("Specific team name to check. Omit to list all active teams."),
348
345
  }),
349
346
  handler: async (args) => {
350
347
  if (args.team_name) {
@@ -368,9 +365,7 @@ export function createTools(deps) {
368
365
  : worker?.status === "error"
369
366
  ? "❌ error"
370
367
  : "🔄 pending";
371
- const elapsed = worker?.startedAt
372
- ? `${Math.round((Date.now() - worker.startedAt) / 1000)}s`
373
- : "";
368
+ const elapsed = worker?.startedAt ? `${Math.round((Date.now() - worker.startedAt) / 1000)}s` : "";
374
369
  lines.push(` ${status} ${memberName} ${elapsed}`);
375
370
  }
376
371
  return lines.join("\n");
package/dist/daemon.js CHANGED
@@ -202,7 +202,7 @@ async function shutdown() {
202
202
  }
203
203
  }
204
204
  // Destroy all active worker sessions to free memory
205
- await Promise.allSettled(Array.from(workers.values()).map((w) => w.session.destroy()));
205
+ await Promise.allSettled(Array.from(workers.values()).map((w) => w.session.disconnect()));
206
206
  workers.clear();
207
207
  try {
208
208
  await stopClient();
@@ -234,7 +234,7 @@ export async function restartDaemon() {
234
234
  }
235
235
  }
236
236
  // Destroy all active worker sessions to free memory
237
- await Promise.allSettled(Array.from(activeWorkers.values()).map((w) => w.session.destroy()));
237
+ await Promise.allSettled(Array.from(activeWorkers.values()).map((w) => w.session.disconnect()));
238
238
  activeWorkers.clear();
239
239
  try {
240
240
  await stopClient();
@@ -23,21 +23,16 @@ export function registerMediaHandlers(bot) {
23
23
  return;
24
24
  }
25
25
  const url = `https://api.telegram.org/file/bot${config.telegramBotToken}/${filePath}`;
26
- const { mkdtempSync, writeFileSync } = await import("fs");
27
- const { join } = await import("path");
28
- const { tmpdir } = await import("os");
29
- const tmpDir = mkdtempSync(join(tmpdir(), "nzb-photo-"));
30
- const ext = filePath.split(".").pop() || "jpg";
31
- const localPath = join(tmpDir, `photo.${ext}`);
32
26
  const response = await fetch(url);
33
27
  const buffer = Buffer.from(await response.arrayBuffer());
34
- writeFileSync(localPath, buffer);
35
- scheduleTempCleanup(tmpDir);
36
- const prompt = `[User sent a photo saved at: ${localPath}]\n\nCaption: ${caption}\n\nPlease analyze this image. The file is at ${localPath} — you can use bash to view it with tools if needed.`;
37
- sendToOrchestrator(prompt, { type: "telegram", chatId, messageId: userMessageId }, (text, done) => {
28
+ const base64Data = buffer.toString("base64");
29
+ const ext = filePath.split(".").pop() || "jpg";
30
+ const mimeType = ext === "png" ? "image/png" : ext === "gif" ? "image/gif" : "image/jpeg";
31
+ const attachment = { type: "blob", data: base64Data, mimeType };
32
+ sendToOrchestrator(caption, { type: "telegram", chatId, messageId: userMessageId }, (text, done) => {
38
33
  if (done)
39
34
  void sendFormattedReply(bot, chatId, text, { replyTo: userMessageId });
40
- });
35
+ }, undefined, undefined, 0, [attachment]);
41
36
  }
42
37
  catch (err) {
43
38
  await ctx.reply(`❌ Error processing photo: ${err instanceof Error ? err.message : String(err)}`, {
@@ -1,3 +1,4 @@
1
+ import { randomUUID } from "node:crypto";
1
2
  import { InlineKeyboard } from "grammy";
2
3
  import { config } from "../../config.js";
3
4
  import { getQueueSize, sendToOrchestrator } from "../../copilot/orchestrator.js";
@@ -75,6 +76,7 @@ export function registerMessageHandler(bot, getBot) {
75
76
  // prevents push notification spam for very short initial chunks (inspired by OpenClaw's draft-stream).
76
77
  const MIN_INITIAL_CHARS = 80;
77
78
  const handlerStartTime = Date.now();
79
+ const requestId = randomUUID();
78
80
  const enqueueEdit = (text) => {
79
81
  if (finalized || text === lastEditedText)
80
82
  return;
@@ -109,6 +111,8 @@ export function registerMessageHandler(bot, getBot) {
109
111
  }
110
112
  }
111
113
  else {
114
+ if (finalized)
115
+ return;
112
116
  try {
113
117
  await editSafe(getBot().api, chatId, placeholderMsgId, safeText);
114
118
  }
@@ -203,80 +207,65 @@ export function registerMessageHandler(bot, getBot) {
203
207
  void logInfo(`✅ Response done (${elapsed}s, ${toolHistory.length} tools, ${text.length} chars)`);
204
208
  // Return the edit chain so callers can await final delivery
205
209
  return editChain.then(async () => {
206
- // Format error messages with a distinct visual
207
- const isError = text.startsWith("Error:");
208
- if (isError) {
209
- void logError(`Response error: ${text.slice(0, 200)}`);
210
- const errorText = `⚠️ ${text}`;
211
- const errorKb = new InlineKeyboard().text("🔄 Retry", "retry").text("📖 Explain", "explain_error");
212
- if (placeholderMsgId) {
210
+ try {
211
+ // Format error messages with a distinct visual
212
+ const isError = text.startsWith("Error:");
213
+ if (isError) {
214
+ void logError(`Response error: ${text.slice(0, 200)}`);
215
+ const errorText = `⚠️ ${text}`;
216
+ const errorKb = new InlineKeyboard().text("🔄 Retry", "retry").text("📖 Explain", "explain_error");
217
+ if (placeholderMsgId) {
218
+ try {
219
+ await editSafe(getBot().api, chatId, placeholderMsgId, errorText, { reply_markup: errorKb });
220
+ return;
221
+ }
222
+ catch {
223
+ /* fall through */
224
+ }
225
+ }
213
226
  try {
214
- await editSafe(getBot().api, chatId, placeholderMsgId, errorText, { reply_markup: errorKb });
215
- return;
227
+ await ctx.reply(errorText, { reply_parameters: replyParams, reply_markup: errorKb });
216
228
  }
217
229
  catch {
218
- /* fall through */
230
+ /* nothing more we can do */
219
231
  }
232
+ return;
220
233
  }
221
- try {
222
- await ctx.reply(errorText, { reply_parameters: replyParams, reply_markup: errorKb });
234
+ let textWithMeta = text;
235
+ if (usageInfo && config.usageMode !== "off") {
236
+ const fmtTokens = (n) => (n >= 1000 ? `${(n / 1000).toFixed(1)}K` : String(n));
237
+ const parts = [];
238
+ if (config.usageMode === "full" && usageInfo.model)
239
+ parts.push(usageInfo.model);
240
+ parts.push(`⬆${fmtTokens(usageInfo.inputTokens)} ⬇${fmtTokens(usageInfo.outputTokens)}`);
241
+ const totalTokens = usageInfo.inputTokens + usageInfo.outputTokens;
242
+ parts.push(`Σ${fmtTokens(totalTokens)}`);
243
+ if (config.usageMode === "full" && usageInfo.duration)
244
+ parts.push(`${(usageInfo.duration / 1000).toFixed(1)}s`);
245
+ textWithMeta += `\n\n📊 ${parts.join(" · ")}`;
223
246
  }
224
- catch {
225
- /* nothing more we can do */
226
- }
227
- return;
228
- }
229
- let textWithMeta = text;
230
- if (usageInfo && config.usageMode !== "off") {
231
- const fmtTokens = (n) => (n >= 1000 ? `${(n / 1000).toFixed(1)}K` : String(n));
232
- const parts = [];
233
- if (config.usageMode === "full" && usageInfo.model)
234
- parts.push(usageInfo.model);
235
- parts.push(`⬆${fmtTokens(usageInfo.inputTokens)} ⬇${fmtTokens(usageInfo.outputTokens)}`);
236
- const totalTokens = usageInfo.inputTokens + usageInfo.outputTokens;
237
- parts.push(`Σ${fmtTokens(totalTokens)}`);
238
- if (config.usageMode === "full" && usageInfo.duration)
239
- parts.push(`${(usageInfo.duration / 1000).toFixed(1)}s`);
240
- textWithMeta += `\n\n📊 ${parts.join(" · ")}`;
241
- }
242
- const formatted = toTelegramHTML(textWithMeta);
243
- let fullFormatted = formatted;
244
- if (config.showReasoning && toolHistory.length > 0) {
245
- const expandable = formatToolSummaryExpandable(toolHistory.map((t) => ({ name: t.name, durationMs: t.durationMs, detail: t.detail })), {
246
- elapsedMs: Date.now() - handlerStartTime,
247
- model: usageInfo?.model,
248
- inputTokens: usageInfo?.inputTokens,
249
- outputTokens: usageInfo?.outputTokens,
250
- });
251
- fullFormatted += expandable;
252
- }
253
- const chunks = chunkMessage(fullFormatted);
254
- const fallbackChunks = chunkMessage(textWithMeta);
255
- // Build smart suggestion buttons based on response content
256
- const smartKb = createSmartSuggestionsWithContext(text, ctx.message.text, 4);
257
- // Single chunk: edit placeholder in place
258
- if (placeholderMsgId && chunks.length === 1) {
259
- try {
260
- await editSafe(getBot().api, chatId, placeholderMsgId, chunks[0], {
261
- parse_mode: "HTML",
262
- reply_markup: smartKb,
247
+ const formatted = toTelegramHTML(textWithMeta);
248
+ let fullFormatted = formatted;
249
+ if (config.showReasoning && toolHistory.length > 0) {
250
+ const expandable = formatToolSummaryExpandable(toolHistory.map((t) => ({ name: t.name, durationMs: t.durationMs, detail: t.detail })), {
251
+ elapsedMs: Date.now() - handlerStartTime,
252
+ model: usageInfo?.model,
253
+ inputTokens: usageInfo?.inputTokens,
254
+ outputTokens: usageInfo?.outputTokens,
263
255
  });
264
- try {
265
- await getBot().api.setMessageReaction(chatId, userMessageId, [{ type: "emoji", emoji: "👍" }]);
266
- }
267
- catch { }
268
- if (assistantLogId) {
269
- try {
270
- const { setConversationTelegramMsgId } = await import("../../store/db.js");
271
- setConversationTelegramMsgId(assistantLogId, placeholderMsgId);
272
- }
273
- catch { }
274
- }
275
- return;
256
+ fullFormatted += expandable;
276
257
  }
277
- catch (err) {
278
- // "message is not modified" is harmless — placeholder already has this content
279
- if (isMessageNotModifiedError(err)) {
258
+ const chunks = chunkMessage(fullFormatted);
259
+ const fallbackChunks = chunkMessage(textWithMeta);
260
+ // Build smart suggestion buttons based on response content
261
+ const smartKb = createSmartSuggestionsWithContext(text, ctx.message.text, 4);
262
+ // Single chunk: edit placeholder in place
263
+ if (placeholderMsgId && chunks.length === 1) {
264
+ try {
265
+ await editSafe(getBot().api, chatId, placeholderMsgId, chunks[0], {
266
+ parse_mode: "HTML",
267
+ reply_markup: smartKb,
268
+ });
280
269
  try {
281
270
  await getBot().api.setMessageReaction(chatId, userMessageId, [{ type: "emoji", emoji: "👍" }]);
282
271
  }
@@ -290,8 +279,45 @@ export function registerMessageHandler(bot, getBot) {
290
279
  }
291
280
  return;
292
281
  }
293
- // HTML parse error — try plain text fallback
294
- if (isHtmlParseError(err)) {
282
+ catch (err) {
283
+ // "message is not modified" is harmless — placeholder already has this content
284
+ if (isMessageNotModifiedError(err)) {
285
+ try {
286
+ await getBot().api.setMessageReaction(chatId, userMessageId, [{ type: "emoji", emoji: "👍" }]);
287
+ }
288
+ catch { }
289
+ if (assistantLogId) {
290
+ try {
291
+ const { setConversationTelegramMsgId } = await import("../../store/db.js");
292
+ setConversationTelegramMsgId(assistantLogId, placeholderMsgId);
293
+ }
294
+ catch { }
295
+ }
296
+ return;
297
+ }
298
+ // HTML parse error — try plain text fallback
299
+ if (isHtmlParseError(err)) {
300
+ try {
301
+ await editSafe(getBot().api, chatId, placeholderMsgId, fallbackChunks[0], {
302
+ reply_markup: smartKb,
303
+ });
304
+ try {
305
+ await getBot().api.setMessageReaction(chatId, userMessageId, [{ type: "emoji", emoji: "👍" }]);
306
+ }
307
+ catch { }
308
+ if (assistantLogId) {
309
+ try {
310
+ const { setConversationTelegramMsgId } = await import("../../store/db.js");
311
+ setConversationTelegramMsgId(assistantLogId, placeholderMsgId);
312
+ }
313
+ catch { }
314
+ }
315
+ return;
316
+ }
317
+ catch {
318
+ /* fall through to send new messages */
319
+ }
320
+ }
295
321
  try {
296
322
  await editSafe(getBot().api, chatId, placeholderMsgId, fallbackChunks[0], {
297
323
  reply_markup: smartKb,
@@ -313,109 +339,93 @@ export function registerMessageHandler(bot, getBot) {
313
339
  /* fall through to send new messages */
314
340
  }
315
341
  }
316
- try {
317
- await editSafe(getBot().api, chatId, placeholderMsgId, fallbackChunks[0], {
318
- reply_markup: smartKb,
319
- });
320
- try {
321
- await getBot().api.setMessageReaction(chatId, userMessageId, [{ type: "emoji", emoji: "👍" }]);
322
- }
323
- catch { }
324
- if (assistantLogId) {
325
- try {
326
- const { setConversationTelegramMsgId } = await import("../../store/db.js");
327
- setConversationTelegramMsgId(assistantLogId, placeholderMsgId);
328
- }
329
- catch { }
330
- }
331
- return;
342
+ }
343
+ // Multi-chunk or edit fallthrough: send new chunks FIRST, then delete placeholder
344
+ const totalChunks = chunks.length;
345
+ let firstSentMsgId;
346
+ const sendChunk = async (chunk, fallback, index) => {
347
+ const isFirst = index === 0 && !placeholderMsgId;
348
+ const isLast = index === totalChunks - 1;
349
+ // Pagination header for multi-chunk messages
350
+ const pageTag = totalChunks > 1 ? `📄 ${index + 1}/${totalChunks}\n` : "";
351
+ // Trim chunk if pageTag pushes it over the limit
352
+ let safeChunk = chunk;
353
+ if (pageTag.length + safeChunk.length > TELEGRAM_MAX_LENGTH) {
354
+ safeChunk = safeChunk.slice(0, TELEGRAM_MAX_LENGTH - pageTag.length - 4) + " ⋯";
332
355
  }
333
- catch {
334
- /* fall through to send new messages */
356
+ let safeFallback = fallback;
357
+ if (pageTag.length + safeFallback.length > TELEGRAM_MAX_LENGTH) {
358
+ safeFallback = safeFallback.slice(0, TELEGRAM_MAX_LENGTH - pageTag.length - 4) + " ⋯";
335
359
  }
336
- }
337
- }
338
- // Multi-chunk or edit fallthrough: send new chunks FIRST, then delete placeholder
339
- const totalChunks = chunks.length;
340
- let firstSentMsgId;
341
- const sendChunk = async (chunk, fallback, index) => {
342
- const isFirst = index === 0 && !placeholderMsgId;
343
- const isLast = index === totalChunks - 1;
344
- // Pagination header for multi-chunk messages
345
- const pageTag = totalChunks > 1 ? `📄 ${index + 1}/${totalChunks}\n` : "";
346
- // Trim chunk if pageTag pushes it over the limit
347
- let safeChunk = chunk;
348
- if (pageTag.length + safeChunk.length > TELEGRAM_MAX_LENGTH) {
349
- safeChunk = safeChunk.slice(0, TELEGRAM_MAX_LENGTH - pageTag.length - 4) + " ⋯";
350
- }
351
- let safeFallback = fallback;
352
- if (pageTag.length + safeFallback.length > TELEGRAM_MAX_LENGTH) {
353
- safeFallback = safeFallback.slice(0, TELEGRAM_MAX_LENGTH - pageTag.length - 4) + " ⋯";
354
- }
355
- const opts = {
356
- parse_mode: "HTML",
357
- ...(isFirst ? { reply_parameters: replyParams } : {}),
358
- ...(isLast && smartKb ? { reply_markup: smartKb } : {}),
359
- };
360
- const fallbackOpts = {
361
- ...(isFirst ? { reply_parameters: replyParams } : {}),
362
- ...(isLast && smartKb ? { reply_markup: smartKb } : {}),
360
+ const opts = {
361
+ parse_mode: "HTML",
362
+ ...(isFirst ? { reply_parameters: replyParams } : {}),
363
+ ...(isLast && smartKb ? { reply_markup: smartKb } : {}),
364
+ };
365
+ const fallbackOpts = {
366
+ ...(isFirst ? { reply_parameters: replyParams } : {}),
367
+ ...(isLast && smartKb ? { reply_markup: smartKb } : {}),
368
+ };
369
+ const sent = await ctx
370
+ .reply(pageTag + safeChunk, opts)
371
+ .catch(() => ctx.reply(pageTag + safeFallback, fallbackOpts));
372
+ if (index === 0 && sent)
373
+ firstSentMsgId = sent.message_id;
363
374
  };
364
- const sent = await ctx
365
- .reply(pageTag + safeChunk, opts)
366
- .catch(() => ctx.reply(pageTag + safeFallback, fallbackOpts));
367
- if (index === 0 && sent)
368
- firstSentMsgId = sent.message_id;
369
- };
370
- let sendSucceeded = false;
371
- try {
372
- for (let i = 0; i < chunks.length; i++) {
373
- if (i > 0)
374
- await new Promise((r) => setTimeout(r, 300));
375
- await sendChunk(chunks[i], fallbackChunks[i] ?? chunks[i], i);
376
- }
377
- sendSucceeded = true;
378
- }
379
- catch {
375
+ let sendSucceeded = false;
380
376
  try {
381
- for (let i = 0; i < fallbackChunks.length; i++) {
377
+ for (let i = 0; i < chunks.length; i++) {
382
378
  if (i > 0)
383
379
  await new Promise((r) => setTimeout(r, 300));
384
- const pageTag = fallbackChunks.length > 1 ? `📄 ${i + 1}/${fallbackChunks.length}\n` : "";
385
- const sent = await ctx.reply(pageTag + fallbackChunks[i], i === 0 ? { reply_parameters: replyParams } : {});
386
- if (i === 0 && sent)
387
- firstSentMsgId = sent.message_id;
380
+ await sendChunk(chunks[i], fallbackChunks[i] ?? chunks[i], i);
388
381
  }
389
382
  sendSucceeded = true;
390
383
  }
391
384
  catch {
392
- /* nothing more we can do */
385
+ try {
386
+ for (let i = 0; i < fallbackChunks.length; i++) {
387
+ if (i > 0)
388
+ await new Promise((r) => setTimeout(r, 300));
389
+ const pageTag = fallbackChunks.length > 1 ? `📄 ${i + 1}/${fallbackChunks.length}\n` : "";
390
+ const sent = await ctx.reply(pageTag + fallbackChunks[i], i === 0 ? { reply_parameters: replyParams } : {});
391
+ if (i === 0 && sent)
392
+ firstSentMsgId = sent.message_id;
393
+ }
394
+ sendSucceeded = true;
395
+ }
396
+ catch {
397
+ /* nothing more we can do */
398
+ }
393
399
  }
394
- }
395
- // Only delete placeholder AFTER new messages sent successfully
396
- if (placeholderMsgId && sendSucceeded) {
397
- try {
398
- await getBot().api.deleteMessage(chatId, placeholderMsgId);
400
+ // Only delete placeholder AFTER new messages sent successfully
401
+ if (placeholderMsgId && sendSucceeded) {
402
+ try {
403
+ await getBot().api.deleteMessage(chatId, placeholderMsgId);
404
+ }
405
+ catch {
406
+ /* ignore — placeholder stays but user has the real message */
407
+ }
399
408
  }
400
- catch {
401
- /* ignore placeholder stays but user has the real message */
409
+ // Track bot message ID for reply-to context lookups
410
+ const botMsgId = firstSentMsgId ?? placeholderMsgId;
411
+ if (assistantLogId && botMsgId) {
412
+ try {
413
+ const { setConversationTelegramMsgId } = await import("../../store/db.js");
414
+ setConversationTelegramMsgId(assistantLogId, botMsgId);
415
+ }
416
+ catch { }
402
417
  }
403
- }
404
- // Track bot message ID for reply-to context lookups
405
- const botMsgId = firstSentMsgId ?? placeholderMsgId;
406
- if (assistantLogId && botMsgId) {
418
+ // React ✅ on the user's original message to signal completion
407
419
  try {
408
- const { setConversationTelegramMsgId } = await import("../../store/db.js");
409
- setConversationTelegramMsgId(assistantLogId, botMsgId);
420
+ await getBot().api.setMessageReaction(chatId, userMessageId, [{ type: "emoji", emoji: "👍" }]);
421
+ }
422
+ catch {
423
+ /* reactions may not be available */
410
424
  }
411
- catch { }
412
- }
413
- // React ✅ on the user's original message to signal completion
414
- try {
415
- await getBot().api.setMessageReaction(chatId, userMessageId, [{ type: "emoji", emoji: "👍" }]);
416
425
  }
417
- catch {
418
- /* reactions may not be available */
426
+ finally {
427
+ placeholderMsgId = undefined;
428
+ lastEditedText = "";
419
429
  }
420
430
  });
421
431
  }
@@ -44,6 +44,7 @@ export function buildSettingsText(getUptimeStr) {
44
44
  `⏱ Worker Timeout: ${getTimeoutLabel()}\n` +
45
45
  `🤖 Model: ${config.copilotModel}\n` +
46
46
  `🧠 Thinking: ${config.thinkingLevel}\n` +
47
+ `💡 Reasoning: ${config.reasoningEffort}\n` +
47
48
  `📝 Verbose: ${config.verboseMode ? "✅ ON" : "❌ OFF"}\n` +
48
49
  `📊 Usage: ${config.usageMode}\n` +
49
50
  `🔧 Show Reasoning: ${config.showReasoning ? "✅ ON" : "❌ OFF"}\n\n` +
@@ -124,6 +125,17 @@ export function createMenus(getUptimeStr) {
124
125
  ctx.menu.update();
125
126
  await ctx.editMessageText(buildSettingsText(getUptimeStr));
126
127
  await ctx.answerCallbackQuery(`Verbose ${config.verboseMode ? "ON" : "OFF"}`);
128
+ })
129
+ .row()
130
+ .text(() => `💡 Reasoning: ${config.reasoningEffort}`, async (ctx) => {
131
+ const efforts = ["low", "medium", "high"];
132
+ const idx = efforts.indexOf(config.reasoningEffort);
133
+ const next = efforts[(idx + 1) % efforts.length];
134
+ config.reasoningEffort = next;
135
+ persistEnvVar("REASONING_EFFORT", next);
136
+ ctx.menu.update();
137
+ await ctx.editMessageText(buildSettingsText(getUptimeStr));
138
+ await ctx.answerCallbackQuery(`Reasoning → ${next}`);
127
139
  })
128
140
  .row()
129
141
  .text(() => `📊 Usage: ${config.usageMode}`, async (ctx) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iletai/nzb",
3
- "version": "1.6.4",
3
+ "version": "1.7.0",
4
4
  "description": "NZB — a personal AI assistant for developers, built on the GitHub Copilot SDK",
5
5
  "bin": {
6
6
  "nzb": "dist/cli.js"
@@ -48,7 +48,7 @@
48
48
  },
49
49
  "type": "module",
50
50
  "dependencies": {
51
- "@github/copilot-sdk": "^0.1.26",
51
+ "@github/copilot-sdk": "^0.2.0",
52
52
  "@grammyjs/auto-retry": "^2.0.2",
53
53
  "@grammyjs/menu": "^1.3.1",
54
54
  "@grammyjs/runner": "^2.0.3",
@@ -69,4 +69,4 @@
69
69
  "typescript": "^5.9.3",
70
70
  "vitest": "^4.1.0"
71
71
  }
72
- }
72
+ }