@iletai/nzb 1.3.5 → 1.3.6

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.
@@ -395,14 +395,14 @@ export async function sendToOrchestrator(prompt, source, callback, onToolEvent,
395
395
  processQueue();
396
396
  });
397
397
  // Deliver response to user FIRST, then log best-effort
398
- callback(finalContent, true);
399
398
  try {
400
399
  logMessage("out", sourceLabel, finalContent);
401
400
  }
402
401
  catch {
403
402
  /* best-effort */
404
403
  }
405
- // Log both sides of the conversation after delivery
404
+ // Log both sides of the conversation before delivery so we have the row ID
405
+ let assistantLogId;
406
406
  try {
407
407
  const telegramMsgId = source.type === "telegram" ? source.messageId : undefined;
408
408
  logConversation(logRole, prompt, sourceLabel, telegramMsgId);
@@ -411,11 +411,12 @@ export async function sendToOrchestrator(prompt, source, callback, onToolEvent,
411
411
  /* best-effort */
412
412
  }
413
413
  try {
414
- logConversation("assistant", finalContent, sourceLabel);
414
+ assistantLogId = logConversation("assistant", finalContent, sourceLabel);
415
415
  }
416
416
  catch {
417
417
  /* best-effort */
418
418
  }
419
+ callback(finalContent, true, { assistantLogId });
419
420
  // Auto-continue: if the response was cut short by timeout, automatically
420
421
  // send a follow-up "Continue" message so the user doesn't have to
421
422
  if (finalContent.includes("⏱ Response was cut short (timeout)")) {
package/dist/store/db.js CHANGED
@@ -100,7 +100,6 @@ export function getDb() {
100
100
  removeMemory: db.prepare(`DELETE FROM memories WHERE id = ?`),
101
101
  memorySummary: db.prepare(`SELECT id, category, content FROM memories ORDER BY category, last_accessed DESC`),
102
102
  getConversationByMsgId: db.prepare(`SELECT id FROM conversation_log WHERE telegram_msg_id = ? LIMIT 1`),
103
- getConversationAround: db.prepare(`SELECT role, content, source, ts FROM conversation_log WHERE id BETWEEN ? - 4 AND ? + 4 ORDER BY id ASC`),
104
103
  };
105
104
  }
106
105
  return db;
@@ -119,23 +118,35 @@ export function deleteState(key) {
119
118
  getDb(); // ensure init
120
119
  stmtCache.deleteState.run(key);
121
120
  }
122
- /** Log a conversation turn (user, assistant, or system) with optional Telegram message ID. */
121
+ /** Log a conversation turn (user, assistant, or system) with optional Telegram message ID. Returns the row ID. */
123
122
  export function logConversation(role, content, source, telegramMsgId) {
124
123
  getDb(); // ensure init
125
- stmtCache.logConversation.run(role, content, source, telegramMsgId ?? null);
124
+ const result = stmtCache.logConversation.run(role, content, source, telegramMsgId ?? null);
126
125
  // Keep last 200 entries to support context recovery after session loss
127
126
  logInsertCount++;
128
127
  if (logInsertCount % 50 === 0) {
129
128
  stmtCache.pruneConversation.run();
130
129
  }
130
+ return result.lastInsertRowid;
131
131
  }
132
- /** Get conversation context around a Telegram message ID (±2 turns). */
132
+ /** Get conversation context around a Telegram message ID (±4 rows using proper subquery). */
133
133
  export function getConversationContext(telegramMsgId) {
134
- getDb(); // ensure init
134
+ const db = getDb();
135
135
  const row = stmtCache.getConversationByMsgId.get(telegramMsgId);
136
136
  if (!row)
137
137
  return undefined;
138
- const rows = stmtCache.getConversationAround.all(row.id, row.id);
138
+ // Fetch 4 rows before + the target + 4 rows after (handles ID gaps from pruning)
139
+ const rows = db.prepare(`
140
+ SELECT role, content, source, ts FROM (
141
+ SELECT * FROM conversation_log WHERE id < ? ORDER BY id DESC LIMIT 4
142
+ )
143
+ UNION ALL
144
+ SELECT role, content, source, ts FROM conversation_log WHERE id = ?
145
+ UNION ALL
146
+ SELECT role, content, source, ts FROM (
147
+ SELECT * FROM conversation_log WHERE id > ? ORDER BY id ASC LIMIT 4
148
+ )
149
+ `).all(row.id, row.id, row.id);
139
150
  if (rows.length === 0)
140
151
  return undefined;
141
152
  return rows
@@ -146,10 +157,10 @@ export function getConversationContext(telegramMsgId) {
146
157
  })
147
158
  .join("\n");
148
159
  }
149
- /** Update the most recent assistant log entry with its Telegram message ID (for reply-to lookups). */
150
- export function updateLastAssistantTelegramMsgId(telegramMsgId) {
160
+ /** Set Telegram message ID on a specific conversation_log row (race-free). */
161
+ export function setConversationTelegramMsgId(rowId, telegramMsgId) {
151
162
  const db = getDb();
152
- db.prepare(`UPDATE conversation_log SET telegram_msg_id = ? WHERE id = (SELECT id FROM conversation_log WHERE role = 'assistant' ORDER BY id DESC LIMIT 1)`).run(telegramMsgId);
163
+ db.prepare(`UPDATE conversation_log SET telegram_msg_id = ? WHERE id = ?`).run(telegramMsgId, rowId);
153
164
  }
154
165
  /** Get recent conversation history formatted for injection into system message. */
155
166
  export function getRecentConversation(limit = 20) {
@@ -454,10 +454,11 @@ export function createBot() {
454
454
  userPrompt = `[Replying to: "${quoted}"]\n\n${userPrompt}`;
455
455
  }
456
456
  }
457
- sendToOrchestrator(userPrompt, { type: "telegram", chatId, messageId: userMessageId }, (text, done) => {
457
+ sendToOrchestrator(userPrompt, { type: "telegram", chatId, messageId: userMessageId }, (text, done, meta) => {
458
458
  if (done) {
459
459
  finalized = true;
460
460
  stopTyping();
461
+ const assistantLogId = meta?.assistantLogId;
461
462
  const elapsed = ((Date.now() - handlerStartTime) / 1000).toFixed(1);
462
463
  void logInfo(`✅ Response done (${elapsed}s, ${toolHistory.length} tools, ${text.length} chars)`);
463
464
  // Wait for in-flight edits to finish before sending the final response
@@ -513,11 +514,13 @@ export function createBot() {
513
514
  await bot.api.setMessageReaction(chatId, userMessageId, [{ type: "emoji", emoji: "👍" }]);
514
515
  }
515
516
  catch { }
516
- try {
517
- const { updateLastAssistantTelegramMsgId } = await import("../store/db.js");
518
- updateLastAssistantTelegramMsgId(placeholderMsgId);
517
+ if (assistantLogId) {
518
+ try {
519
+ const { setConversationTelegramMsgId } = await import("../store/db.js");
520
+ setConversationTelegramMsgId(assistantLogId, placeholderMsgId);
521
+ }
522
+ catch { }
519
523
  }
520
- catch { }
521
524
  return;
522
525
  }
523
526
  catch {
@@ -527,11 +530,13 @@ export function createBot() {
527
530
  await bot.api.setMessageReaction(chatId, userMessageId, [{ type: "emoji", emoji: "👍" }]);
528
531
  }
529
532
  catch { }
530
- try {
531
- const { updateLastAssistantTelegramMsgId } = await import("../store/db.js");
532
- updateLastAssistantTelegramMsgId(placeholderMsgId);
533
+ if (assistantLogId) {
534
+ try {
535
+ const { setConversationTelegramMsgId } = await import("../store/db.js");
536
+ setConversationTelegramMsgId(assistantLogId, placeholderMsgId);
537
+ }
538
+ catch { }
533
539
  }
534
- catch { }
535
540
  return;
536
541
  }
537
542
  catch {
@@ -541,6 +546,7 @@ export function createBot() {
541
546
  }
542
547
  // Multi-chunk or edit fallthrough: send new chunks FIRST, then delete placeholder
543
548
  const totalChunks = chunks.length;
549
+ let firstSentMsgId;
544
550
  const sendChunk = async (chunk, fallback, index) => {
545
551
  const isFirst = index === 0 && !placeholderMsgId;
546
552
  // Pagination header for multi-chunk messages
@@ -548,9 +554,11 @@ export function createBot() {
548
554
  const opts = isFirst
549
555
  ? { parse_mode: "MarkdownV2", reply_parameters: replyParams }
550
556
  : { parse_mode: "MarkdownV2" };
551
- await ctx
557
+ const sent = await ctx
552
558
  .reply(pageTag + chunk, opts)
553
559
  .catch(() => ctx.reply(pageTag + fallback, isFirst ? { reply_parameters: replyParams } : {}));
560
+ if (index === 0 && sent)
561
+ firstSentMsgId = sent.message_id;
554
562
  };
555
563
  let sendSucceeded = false;
556
564
  try {
@@ -567,7 +575,9 @@ export function createBot() {
567
575
  if (i > 0)
568
576
  await new Promise((r) => setTimeout(r, 300));
569
577
  const pageTag = fallbackChunks.length > 1 ? `📄 ${i + 1}/${fallbackChunks.length}\n` : "";
570
- await ctx.reply(pageTag + fallbackChunks[i], i === 0 ? { reply_parameters: replyParams } : {});
578
+ const sent = await ctx.reply(pageTag + fallbackChunks[i], i === 0 ? { reply_parameters: replyParams } : {});
579
+ if (i === 0 && sent)
580
+ firstSentMsgId = sent.message_id;
571
581
  }
572
582
  sendSucceeded = true;
573
583
  }
@@ -584,6 +594,15 @@ export function createBot() {
584
594
  /* ignore — placeholder stays but user has the real message */
585
595
  }
586
596
  }
597
+ // Track bot message ID for reply-to context lookups
598
+ const botMsgId = firstSentMsgId ?? placeholderMsgId;
599
+ if (assistantLogId && botMsgId) {
600
+ try {
601
+ const { setConversationTelegramMsgId } = await import("../store/db.js");
602
+ setConversationTelegramMsgId(assistantLogId, botMsgId);
603
+ }
604
+ catch { }
605
+ }
587
606
  // React ✅ on the user's original message to signal completion
588
607
  try {
589
608
  await bot.api.setMessageReaction(chatId, userMessageId, [{ type: "emoji", emoji: "👍" }]);
@@ -769,6 +788,20 @@ export function createBot() {
769
788
  await ctx.reply("❌ Voice too long (max 5 min).", { reply_parameters: { message_id: userMessageId } });
770
789
  return;
771
790
  }
791
+ // If voice is a reply, include context
792
+ let voiceReplyContext = "";
793
+ const voiceReplyMsg = ctx.message.reply_to_message;
794
+ if (voiceReplyMsg && "text" in voiceReplyMsg && voiceReplyMsg.text) {
795
+ const { getConversationContext } = await import("../store/db.js");
796
+ const context = getConversationContext(voiceReplyMsg.message_id);
797
+ if (context) {
798
+ voiceReplyContext = `[Continuing from earlier conversation:]\n---\n${context}\n---\n\n`;
799
+ }
800
+ else {
801
+ const quoted = voiceReplyMsg.text.length > 500 ? voiceReplyMsg.text.slice(0, 500) + "…" : voiceReplyMsg.text;
802
+ voiceReplyContext = `[Replying to: "${quoted}"]\n\n`;
803
+ }
804
+ }
772
805
  try {
773
806
  const file = await ctx.api.getFile(ctx.message.voice.file_id);
774
807
  const filePath = file.file_path;
@@ -819,31 +852,44 @@ export function createBot() {
819
852
  else {
820
853
  prompt = `[User sent a voice message (${duration}s), saved at: ${localPath}. No OPENAI_API_KEY configured for transcription. You can tell the user to set it up in ~/.nzb/.env]`;
821
854
  }
822
- sendToOrchestrator(prompt, { type: "telegram", chatId, messageId: userMessageId }, (text, done) => {
855
+ sendToOrchestrator(voiceReplyContext + prompt, { type: "telegram", chatId, messageId: userMessageId }, (text, done, meta) => {
823
856
  if (done) {
857
+ const assistantLogId = meta?.assistantLogId;
824
858
  const formatted = toTelegramMarkdown(text);
825
859
  const chunks = chunkMessage(formatted);
826
860
  const fallbackChunks = chunkMessage(text);
827
861
  void (async () => {
862
+ let firstMsgId;
828
863
  for (let i = 0; i < chunks.length; i++) {
829
864
  if (i > 0)
830
865
  await new Promise((r) => setTimeout(r, 300));
831
866
  const pageTag = chunks.length > 1 ? `📄 ${i + 1}/${chunks.length}\n` : "";
832
867
  try {
833
- await ctx.api.sendMessage(chatId, pageTag + chunks[i], {
868
+ const sent = await ctx.api.sendMessage(chatId, pageTag + chunks[i], {
834
869
  parse_mode: "MarkdownV2",
835
870
  reply_parameters: i === 0 ? { message_id: userMessageId } : undefined,
836
871
  });
872
+ if (i === 0)
873
+ firstMsgId = sent.message_id;
837
874
  }
838
875
  catch {
839
876
  try {
840
- await ctx.api.sendMessage(chatId, pageTag + (fallbackChunks[i] ?? chunks[i]), {
877
+ const sent = await ctx.api.sendMessage(chatId, pageTag + (fallbackChunks[i] ?? chunks[i]), {
841
878
  reply_parameters: i === 0 ? { message_id: userMessageId } : undefined,
842
879
  });
880
+ if (i === 0)
881
+ firstMsgId = sent.message_id;
843
882
  }
844
883
  catch { }
845
884
  }
846
885
  }
886
+ if (assistantLogId && firstMsgId) {
887
+ try {
888
+ const { setConversationTelegramMsgId } = await import("../store/db.js");
889
+ setConversationTelegramMsgId(assistantLogId, firstMsgId);
890
+ }
891
+ catch { }
892
+ }
847
893
  try {
848
894
  await ctx.api.setMessageReaction(chatId, userMessageId, [{ type: "emoji", emoji: "👍" }]);
849
895
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iletai/nzb",
3
- "version": "1.3.5",
3
+ "version": "1.3.6",
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"