@iletai/nzb 1.3.4 → 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.
- package/dist/copilot/orchestrator.js +6 -4
- package/dist/store/db.js +50 -4
- package/dist/telegram/bot.js +73 -9
- package/package.json +1 -1
|
@@ -395,26 +395,28 @@ 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
|
|
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
|
+
logConversation(logRole, prompt, sourceLabel, telegramMsgId);
|
|
408
409
|
}
|
|
409
410
|
catch {
|
|
410
411
|
/* best-effort */
|
|
411
412
|
}
|
|
412
413
|
try {
|
|
413
|
-
logConversation("assistant", finalContent, sourceLabel);
|
|
414
|
+
assistantLogId = logConversation("assistant", finalContent, sourceLabel);
|
|
414
415
|
}
|
|
415
416
|
catch {
|
|
416
417
|
/* best-effort */
|
|
417
418
|
}
|
|
419
|
+
callback(finalContent, true, { assistantLogId });
|
|
418
420
|
// Auto-continue: if the response was cut short by timeout, automatically
|
|
419
421
|
// send a follow-up "Continue" message so the user doesn't have to
|
|
420
422
|
if (finalContent.includes("⏱ Response was cut short (timeout)")) {
|
package/dist/store/db.js
CHANGED
|
@@ -33,9 +33,19 @@ export function getDb() {
|
|
|
33
33
|
role TEXT NOT NULL CHECK(role IN ('user', 'assistant', 'system')),
|
|
34
34
|
content TEXT NOT NULL,
|
|
35
35
|
source TEXT NOT NULL DEFAULT 'unknown',
|
|
36
|
+
telegram_msg_id INTEGER,
|
|
36
37
|
ts DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
37
38
|
)
|
|
38
39
|
`);
|
|
40
|
+
// Migrate: add telegram_msg_id column if missing
|
|
41
|
+
try {
|
|
42
|
+
db.prepare(`SELECT telegram_msg_id FROM conversation_log LIMIT 1`).get();
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
db.exec(`ALTER TABLE conversation_log ADD COLUMN telegram_msg_id INTEGER`);
|
|
46
|
+
}
|
|
47
|
+
// Index for fast reply-to lookups
|
|
48
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_conv_telegram_msg ON conversation_log (telegram_msg_id)`);
|
|
39
49
|
db.exec(`
|
|
40
50
|
CREATE TABLE IF NOT EXISTS memories (
|
|
41
51
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
@@ -60,6 +70,7 @@ export function getDb() {
|
|
|
60
70
|
role TEXT NOT NULL CHECK(role IN ('user', 'assistant', 'system')),
|
|
61
71
|
content TEXT NOT NULL,
|
|
62
72
|
source TEXT NOT NULL DEFAULT 'unknown',
|
|
73
|
+
telegram_msg_id INTEGER,
|
|
63
74
|
ts DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
64
75
|
)
|
|
65
76
|
`);
|
|
@@ -83,11 +94,12 @@ export function getDb() {
|
|
|
83
94
|
getState: db.prepare(`SELECT value FROM nzb_state WHERE key = ?`),
|
|
84
95
|
setState: db.prepare(`INSERT OR REPLACE INTO nzb_state (key, value) VALUES (?, ?)`),
|
|
85
96
|
deleteState: db.prepare(`DELETE FROM nzb_state WHERE key = ?`),
|
|
86
|
-
logConversation: db.prepare(`INSERT INTO conversation_log (role, content, source) VALUES (?, ?, ?)`),
|
|
97
|
+
logConversation: db.prepare(`INSERT INTO conversation_log (role, content, source, telegram_msg_id) VALUES (?, ?, ?, ?)`),
|
|
87
98
|
pruneConversation: db.prepare(`DELETE FROM conversation_log WHERE id NOT IN (SELECT id FROM conversation_log ORDER BY id DESC LIMIT 200)`),
|
|
88
99
|
addMemory: db.prepare(`INSERT INTO memories (category, content, source) VALUES (?, ?, ?)`),
|
|
89
100
|
removeMemory: db.prepare(`DELETE FROM memories WHERE id = ?`),
|
|
90
101
|
memorySummary: db.prepare(`SELECT id, category, content FROM memories ORDER BY category, last_accessed DESC`),
|
|
102
|
+
getConversationByMsgId: db.prepare(`SELECT id FROM conversation_log WHERE telegram_msg_id = ? LIMIT 1`),
|
|
91
103
|
};
|
|
92
104
|
}
|
|
93
105
|
return db;
|
|
@@ -106,15 +118,49 @@ export function deleteState(key) {
|
|
|
106
118
|
getDb(); // ensure init
|
|
107
119
|
stmtCache.deleteState.run(key);
|
|
108
120
|
}
|
|
109
|
-
/** Log a conversation turn (user, assistant, or system). */
|
|
110
|
-
export function logConversation(role, content, source) {
|
|
121
|
+
/** Log a conversation turn (user, assistant, or system) with optional Telegram message ID. Returns the row ID. */
|
|
122
|
+
export function logConversation(role, content, source, telegramMsgId) {
|
|
111
123
|
getDb(); // ensure init
|
|
112
|
-
stmtCache.logConversation.run(role, content, source);
|
|
124
|
+
const result = stmtCache.logConversation.run(role, content, source, telegramMsgId ?? null);
|
|
113
125
|
// Keep last 200 entries to support context recovery after session loss
|
|
114
126
|
logInsertCount++;
|
|
115
127
|
if (logInsertCount % 50 === 0) {
|
|
116
128
|
stmtCache.pruneConversation.run();
|
|
117
129
|
}
|
|
130
|
+
return result.lastInsertRowid;
|
|
131
|
+
}
|
|
132
|
+
/** Get conversation context around a Telegram message ID (±4 rows using proper subquery). */
|
|
133
|
+
export function getConversationContext(telegramMsgId) {
|
|
134
|
+
const db = getDb();
|
|
135
|
+
const row = stmtCache.getConversationByMsgId.get(telegramMsgId);
|
|
136
|
+
if (!row)
|
|
137
|
+
return undefined;
|
|
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);
|
|
150
|
+
if (rows.length === 0)
|
|
151
|
+
return undefined;
|
|
152
|
+
return rows
|
|
153
|
+
.map((r) => {
|
|
154
|
+
const tag = r.role === "user" ? "You" : r.role === "assistant" ? "NZB" : "System";
|
|
155
|
+
const content = r.content.length > 400 ? r.content.slice(0, 400) + "…" : r.content;
|
|
156
|
+
return `${tag}: ${content}`;
|
|
157
|
+
})
|
|
158
|
+
.join("\n");
|
|
159
|
+
}
|
|
160
|
+
/** Set Telegram message ID on a specific conversation_log row (race-free). */
|
|
161
|
+
export function setConversationTelegramMsgId(rowId, telegramMsgId) {
|
|
162
|
+
const db = getDb();
|
|
163
|
+
db.prepare(`UPDATE conversation_log SET telegram_msg_id = ? WHERE id = ?`).run(telegramMsgId, rowId);
|
|
118
164
|
}
|
|
119
165
|
/** Get recent conversation history formatted for injection into system message. */
|
|
120
166
|
export function getRecentConversation(limit = 20) {
|
package/dist/telegram/bot.js
CHANGED
|
@@ -439,17 +439,26 @@ export function createBot() {
|
|
|
439
439
|
const onUsage = (usage) => {
|
|
440
440
|
usageInfo = usage;
|
|
441
441
|
};
|
|
442
|
-
// If user replies to a message, include
|
|
442
|
+
// If user replies to a message, include surrounding conversation context
|
|
443
443
|
let userPrompt = ctx.message.text;
|
|
444
444
|
const replyMsg = ctx.message.reply_to_message;
|
|
445
445
|
if (replyMsg && "text" in replyMsg && replyMsg.text) {
|
|
446
|
-
|
|
447
|
-
|
|
446
|
+
// Try to find full conversation context around the replied message
|
|
447
|
+
const { getConversationContext } = await import("../store/db.js");
|
|
448
|
+
const context = getConversationContext(replyMsg.message_id);
|
|
449
|
+
if (context) {
|
|
450
|
+
userPrompt = `[Continuing from earlier conversation:]\n---\n${context}\n---\n\n[Your reply]: ${userPrompt}`;
|
|
451
|
+
}
|
|
452
|
+
else {
|
|
453
|
+
const quoted = replyMsg.text.length > 500 ? replyMsg.text.slice(0, 500) + "…" : replyMsg.text;
|
|
454
|
+
userPrompt = `[Replying to: "${quoted}"]\n\n${userPrompt}`;
|
|
455
|
+
}
|
|
448
456
|
}
|
|
449
|
-
sendToOrchestrator(userPrompt, { type: "telegram", chatId, messageId: userMessageId }, (text, done) => {
|
|
457
|
+
sendToOrchestrator(userPrompt, { type: "telegram", chatId, messageId: userMessageId }, (text, done, meta) => {
|
|
450
458
|
if (done) {
|
|
451
459
|
finalized = true;
|
|
452
460
|
stopTyping();
|
|
461
|
+
const assistantLogId = meta?.assistantLogId;
|
|
453
462
|
const elapsed = ((Date.now() - handlerStartTime) / 1000).toFixed(1);
|
|
454
463
|
void logInfo(`✅ Response done (${elapsed}s, ${toolHistory.length} tools, ${text.length} chars)`);
|
|
455
464
|
// Wait for in-flight edits to finish before sending the final response
|
|
@@ -505,6 +514,13 @@ export function createBot() {
|
|
|
505
514
|
await bot.api.setMessageReaction(chatId, userMessageId, [{ type: "emoji", emoji: "👍" }]);
|
|
506
515
|
}
|
|
507
516
|
catch { }
|
|
517
|
+
if (assistantLogId) {
|
|
518
|
+
try {
|
|
519
|
+
const { setConversationTelegramMsgId } = await import("../store/db.js");
|
|
520
|
+
setConversationTelegramMsgId(assistantLogId, placeholderMsgId);
|
|
521
|
+
}
|
|
522
|
+
catch { }
|
|
523
|
+
}
|
|
508
524
|
return;
|
|
509
525
|
}
|
|
510
526
|
catch {
|
|
@@ -514,6 +530,13 @@ export function createBot() {
|
|
|
514
530
|
await bot.api.setMessageReaction(chatId, userMessageId, [{ type: "emoji", emoji: "👍" }]);
|
|
515
531
|
}
|
|
516
532
|
catch { }
|
|
533
|
+
if (assistantLogId) {
|
|
534
|
+
try {
|
|
535
|
+
const { setConversationTelegramMsgId } = await import("../store/db.js");
|
|
536
|
+
setConversationTelegramMsgId(assistantLogId, placeholderMsgId);
|
|
537
|
+
}
|
|
538
|
+
catch { }
|
|
539
|
+
}
|
|
517
540
|
return;
|
|
518
541
|
}
|
|
519
542
|
catch {
|
|
@@ -523,6 +546,7 @@ export function createBot() {
|
|
|
523
546
|
}
|
|
524
547
|
// Multi-chunk or edit fallthrough: send new chunks FIRST, then delete placeholder
|
|
525
548
|
const totalChunks = chunks.length;
|
|
549
|
+
let firstSentMsgId;
|
|
526
550
|
const sendChunk = async (chunk, fallback, index) => {
|
|
527
551
|
const isFirst = index === 0 && !placeholderMsgId;
|
|
528
552
|
// Pagination header for multi-chunk messages
|
|
@@ -530,9 +554,11 @@ export function createBot() {
|
|
|
530
554
|
const opts = isFirst
|
|
531
555
|
? { parse_mode: "MarkdownV2", reply_parameters: replyParams }
|
|
532
556
|
: { parse_mode: "MarkdownV2" };
|
|
533
|
-
await ctx
|
|
557
|
+
const sent = await ctx
|
|
534
558
|
.reply(pageTag + chunk, opts)
|
|
535
559
|
.catch(() => ctx.reply(pageTag + fallback, isFirst ? { reply_parameters: replyParams } : {}));
|
|
560
|
+
if (index === 0 && sent)
|
|
561
|
+
firstSentMsgId = sent.message_id;
|
|
536
562
|
};
|
|
537
563
|
let sendSucceeded = false;
|
|
538
564
|
try {
|
|
@@ -549,7 +575,9 @@ export function createBot() {
|
|
|
549
575
|
if (i > 0)
|
|
550
576
|
await new Promise((r) => setTimeout(r, 300));
|
|
551
577
|
const pageTag = fallbackChunks.length > 1 ? `📄 ${i + 1}/${fallbackChunks.length}\n` : "";
|
|
552
|
-
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;
|
|
553
581
|
}
|
|
554
582
|
sendSucceeded = true;
|
|
555
583
|
}
|
|
@@ -566,6 +594,15 @@ export function createBot() {
|
|
|
566
594
|
/* ignore — placeholder stays but user has the real message */
|
|
567
595
|
}
|
|
568
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
|
+
}
|
|
569
606
|
// React ✅ on the user's original message to signal completion
|
|
570
607
|
try {
|
|
571
608
|
await bot.api.setMessageReaction(chatId, userMessageId, [{ type: "emoji", emoji: "👍" }]);
|
|
@@ -751,6 +788,20 @@ export function createBot() {
|
|
|
751
788
|
await ctx.reply("❌ Voice too long (max 5 min).", { reply_parameters: { message_id: userMessageId } });
|
|
752
789
|
return;
|
|
753
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
|
+
}
|
|
754
805
|
try {
|
|
755
806
|
const file = await ctx.api.getFile(ctx.message.voice.file_id);
|
|
756
807
|
const filePath = file.file_path;
|
|
@@ -801,31 +852,44 @@ export function createBot() {
|
|
|
801
852
|
else {
|
|
802
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]`;
|
|
803
854
|
}
|
|
804
|
-
sendToOrchestrator(prompt, { type: "telegram", chatId, messageId: userMessageId }, (text, done) => {
|
|
855
|
+
sendToOrchestrator(voiceReplyContext + prompt, { type: "telegram", chatId, messageId: userMessageId }, (text, done, meta) => {
|
|
805
856
|
if (done) {
|
|
857
|
+
const assistantLogId = meta?.assistantLogId;
|
|
806
858
|
const formatted = toTelegramMarkdown(text);
|
|
807
859
|
const chunks = chunkMessage(formatted);
|
|
808
860
|
const fallbackChunks = chunkMessage(text);
|
|
809
861
|
void (async () => {
|
|
862
|
+
let firstMsgId;
|
|
810
863
|
for (let i = 0; i < chunks.length; i++) {
|
|
811
864
|
if (i > 0)
|
|
812
865
|
await new Promise((r) => setTimeout(r, 300));
|
|
813
866
|
const pageTag = chunks.length > 1 ? `📄 ${i + 1}/${chunks.length}\n` : "";
|
|
814
867
|
try {
|
|
815
|
-
await ctx.api.sendMessage(chatId, pageTag + chunks[i], {
|
|
868
|
+
const sent = await ctx.api.sendMessage(chatId, pageTag + chunks[i], {
|
|
816
869
|
parse_mode: "MarkdownV2",
|
|
817
870
|
reply_parameters: i === 0 ? { message_id: userMessageId } : undefined,
|
|
818
871
|
});
|
|
872
|
+
if (i === 0)
|
|
873
|
+
firstMsgId = sent.message_id;
|
|
819
874
|
}
|
|
820
875
|
catch {
|
|
821
876
|
try {
|
|
822
|
-
await ctx.api.sendMessage(chatId, pageTag + (fallbackChunks[i] ?? chunks[i]), {
|
|
877
|
+
const sent = await ctx.api.sendMessage(chatId, pageTag + (fallbackChunks[i] ?? chunks[i]), {
|
|
823
878
|
reply_parameters: i === 0 ? { message_id: userMessageId } : undefined,
|
|
824
879
|
});
|
|
880
|
+
if (i === 0)
|
|
881
|
+
firstMsgId = sent.message_id;
|
|
825
882
|
}
|
|
826
883
|
catch { }
|
|
827
884
|
}
|
|
828
885
|
}
|
|
886
|
+
if (assistantLogId && firstMsgId) {
|
|
887
|
+
try {
|
|
888
|
+
const { setConversationTelegramMsgId } = await import("../store/db.js");
|
|
889
|
+
setConversationTelegramMsgId(assistantLogId, firstMsgId);
|
|
890
|
+
}
|
|
891
|
+
catch { }
|
|
892
|
+
}
|
|
829
893
|
try {
|
|
830
894
|
await ctx.api.setMessageReaction(chatId, userMessageId, [{ type: "emoji", emoji: "👍" }]);
|
|
831
895
|
}
|