@iletai/nzb 1.7.0 → 1.7.4
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/api/server.js +37 -6
- package/dist/cli.js +1 -0
- package/dist/config.js +12 -3
- package/dist/copilot/client.js +17 -16
- package/dist/copilot/mcp-config.js +2 -0
- package/dist/copilot/orchestrator.js +289 -125
- package/dist/copilot/skills.js +4 -2
- package/dist/copilot/tools.js +48 -11
- package/dist/copilot/types.js +2 -0
- package/dist/daemon.js +11 -10
- package/dist/setup.js +3 -2
- package/dist/store/conversation.js +96 -0
- package/dist/store/db.js +7 -206
- package/dist/store/memory.js +90 -0
- package/dist/store/team-store.js +51 -0
- package/dist/telegram/bot.js +85 -8
- package/dist/telegram/handlers/commands.js +1 -1
- package/dist/telegram/handlers/media.js +63 -6
- package/dist/telegram/handlers/streaming.js +223 -188
- package/dist/telegram/handlers/suggestions.js +22 -1
- package/dist/telegram/log-channel.js +2 -2
- package/dist/telegram/menus.js +243 -99
- package/dist/tui/ansi.js +19 -0
- package/dist/tui/api-client.js +158 -0
- package/dist/tui/debug.js +27 -0
- package/dist/tui/renderer.js +59 -0
- package/dist/tui/stream.js +163 -0
- package/dist/update.js +2 -0
- package/dist/utils.js +102 -0
- package/package.json +1 -1
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { approveAll } from "@github/copilot-sdk";
|
|
2
2
|
import { config, DEFAULT_MODEL } from "../config.js";
|
|
3
3
|
import { SESSIONS_DIR } from "../paths.js";
|
|
4
|
-
import {
|
|
4
|
+
import { deleteState, getState, setState } from "../store/db.js";
|
|
5
|
+
import { getRecentConversation, logConversation } from "../store/conversation.js";
|
|
6
|
+
import { getMemorySummary } from "../store/memory.js";
|
|
7
|
+
import { completeTeam, updateTeamMemberResult } from "../store/team-store.js";
|
|
8
|
+
import { formatAge, withTimeout } from "../utils.js";
|
|
5
9
|
import { resetClient } from "./client.js";
|
|
6
10
|
import { loadMcpConfig } from "./mcp-config.js";
|
|
7
11
|
import { getSkillDirectories } from "./skills.js";
|
|
@@ -27,10 +31,13 @@ let copilotClient;
|
|
|
27
31
|
const workers = new Map();
|
|
28
32
|
const teams = new Map();
|
|
29
33
|
let healthCheckTimer;
|
|
34
|
+
let workerReaperTimer;
|
|
30
35
|
// Persistent orchestrator session
|
|
31
36
|
let orchestratorSession;
|
|
32
37
|
// Coalesces concurrent ensureOrchestratorSession calls
|
|
33
38
|
let sessionCreatePromise;
|
|
39
|
+
// Tracks when the orchestrator session was created for TTL enforcement
|
|
40
|
+
let sessionCreatedAt;
|
|
34
41
|
// Tracks in-flight context recovery injection so we don't race with real messages
|
|
35
42
|
let recoveryInjectionPromise;
|
|
36
43
|
const messageQueue = [];
|
|
@@ -60,6 +67,7 @@ function getSessionConfig() {
|
|
|
60
67
|
workerNotifyFn(event, channel);
|
|
61
68
|
}
|
|
62
69
|
},
|
|
70
|
+
getCurrentSourceChannel,
|
|
63
71
|
});
|
|
64
72
|
cachedToolsClientRef = copilotClient;
|
|
65
73
|
}
|
|
@@ -138,27 +146,37 @@ async function ensureClient() {
|
|
|
138
146
|
return resetPromise;
|
|
139
147
|
}
|
|
140
148
|
/** Start periodic health check that proactively reconnects the client. */
|
|
149
|
+
let healthCheckRunning = false;
|
|
141
150
|
function startHealthCheck() {
|
|
142
151
|
if (healthCheckTimer)
|
|
143
152
|
return;
|
|
144
153
|
healthCheckTimer = setInterval(async () => {
|
|
145
154
|
if (!copilotClient)
|
|
146
155
|
return;
|
|
156
|
+
if (healthCheckRunning)
|
|
157
|
+
return;
|
|
158
|
+
if (processing)
|
|
159
|
+
return; // Don't interfere while processing messages
|
|
160
|
+
healthCheckRunning = true;
|
|
147
161
|
try {
|
|
148
162
|
const state = copilotClient.getState();
|
|
149
163
|
if (state !== "connected") {
|
|
150
164
|
console.log(`[nzb] Health check: client state is '${state}', resetting…`);
|
|
151
165
|
const previousClient = copilotClient;
|
|
152
|
-
await ensureClient();
|
|
166
|
+
await withTimeout(ensureClient(), 15_000, "health check");
|
|
153
167
|
// Only invalidate session if the underlying client actually changed
|
|
154
168
|
if (copilotClient !== previousClient) {
|
|
155
169
|
orchestratorSession = undefined;
|
|
170
|
+
sessionCreatedAt = undefined;
|
|
156
171
|
}
|
|
157
172
|
}
|
|
158
173
|
}
|
|
159
174
|
catch (err) {
|
|
160
175
|
console.error(`[nzb] Health check error:`, err instanceof Error ? err.message : err);
|
|
161
176
|
}
|
|
177
|
+
finally {
|
|
178
|
+
healthCheckRunning = false;
|
|
179
|
+
}
|
|
162
180
|
}, HEALTH_CHECK_INTERVAL_MS);
|
|
163
181
|
}
|
|
164
182
|
/** Stop the periodic health check timer. Call during shutdown. */
|
|
@@ -168,17 +186,64 @@ export function stopHealthCheck() {
|
|
|
168
186
|
healthCheckTimer = undefined;
|
|
169
187
|
}
|
|
170
188
|
}
|
|
189
|
+
/** Periodically kills workers that have exceeded 2× their configured timeout. */
|
|
190
|
+
function startWorkerReaper() {
|
|
191
|
+
if (workerReaperTimer)
|
|
192
|
+
return;
|
|
193
|
+
workerReaperTimer = setInterval(async () => {
|
|
194
|
+
const maxAge = config.workerTimeoutMs * 2;
|
|
195
|
+
const now = Date.now();
|
|
196
|
+
for (const [name, worker] of workers) {
|
|
197
|
+
if (worker.startedAt && now - worker.startedAt > maxAge) {
|
|
198
|
+
console.log(`[nzb] Reaping stuck worker '${name}' (age: ${formatAge(worker.startedAt)})`);
|
|
199
|
+
try {
|
|
200
|
+
await withTimeout(worker.session.disconnect(), 5_000, `reaper: worker '${name}'`);
|
|
201
|
+
}
|
|
202
|
+
catch (err) {
|
|
203
|
+
console.error(`[nzb] Reaper: worker '${name}' disconnect failed:`, err instanceof Error ? err.message : err);
|
|
204
|
+
}
|
|
205
|
+
workers.delete(name);
|
|
206
|
+
feedBackgroundResult(name, `⚠ Worker '${name}' was automatically killed after exceeding timeout.`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}, 5 * 60 * 1000);
|
|
210
|
+
workerReaperTimer.unref();
|
|
211
|
+
}
|
|
212
|
+
const SESSION_MAX_AGE_MS = 4 * 60 * 60 * 1000; // 4 hours
|
|
171
213
|
/** Create or resume the persistent orchestrator session. */
|
|
172
214
|
async function ensureOrchestratorSession() {
|
|
173
|
-
if (orchestratorSession)
|
|
174
|
-
|
|
215
|
+
if (orchestratorSession) {
|
|
216
|
+
// Validate session is still usable — check client connectivity
|
|
217
|
+
try {
|
|
218
|
+
const clientState = copilotClient?.getState?.();
|
|
219
|
+
if (clientState && clientState !== "connected") {
|
|
220
|
+
console.log(`[nzb] Session stale (client state: ${clientState}), recreating…`);
|
|
221
|
+
orchestratorSession = undefined;
|
|
222
|
+
sessionCreatedAt = undefined;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
console.log("[nzb] Session validation failed, recreating…");
|
|
227
|
+
orchestratorSession = undefined;
|
|
228
|
+
sessionCreatedAt = undefined;
|
|
229
|
+
}
|
|
230
|
+
// Enforce session TTL
|
|
231
|
+
if (sessionCreatedAt && Date.now() - sessionCreatedAt > SESSION_MAX_AGE_MS) {
|
|
232
|
+
console.log("[nzb] Session TTL expired, recreating…");
|
|
233
|
+
orchestratorSession = undefined;
|
|
234
|
+
sessionCreatedAt = undefined;
|
|
235
|
+
}
|
|
236
|
+
if (orchestratorSession)
|
|
237
|
+
return orchestratorSession;
|
|
238
|
+
}
|
|
175
239
|
// Coalesce concurrent callers — wait for an in-flight creation
|
|
176
240
|
if (sessionCreatePromise)
|
|
177
241
|
return sessionCreatePromise;
|
|
178
|
-
sessionCreatePromise = createOrResumeSession();
|
|
242
|
+
sessionCreatePromise = withTimeout(createOrResumeSession(), 30_000, "session create/resume");
|
|
179
243
|
try {
|
|
180
244
|
const session = await sessionCreatePromise;
|
|
181
245
|
orchestratorSession = session;
|
|
246
|
+
sessionCreatedAt = Date.now();
|
|
182
247
|
return session;
|
|
183
248
|
}
|
|
184
249
|
finally {
|
|
@@ -250,7 +315,7 @@ async function createOrResumeSession() {
|
|
|
250
315
|
// Recover conversation context if available (session was lost, not first run)
|
|
251
316
|
// Runs concurrently but is awaited before any real message is sent on the session
|
|
252
317
|
const recentHistory = getRecentConversation(10);
|
|
253
|
-
if (recentHistory) {
|
|
318
|
+
if (!recoveryInjectionPromise && recentHistory) {
|
|
254
319
|
console.log(`[nzb] Injecting recent conversation context into new session`);
|
|
255
320
|
recoveryInjectionPromise = session
|
|
256
321
|
.sendAndWait({
|
|
@@ -290,6 +355,7 @@ export async function initOrchestrator(client) {
|
|
|
290
355
|
console.log(`[nzb] Skill directories: ${skillDirectories.join(", ") || "(none)"}`);
|
|
291
356
|
console.log(`[nzb] Persistent session mode — conversation history maintained by SDK`);
|
|
292
357
|
startHealthCheck();
|
|
358
|
+
startWorkerReaper();
|
|
293
359
|
// Eagerly create/resume the orchestrator session
|
|
294
360
|
try {
|
|
295
361
|
await ensureOrchestratorSession();
|
|
@@ -303,53 +369,85 @@ async function executeOnSession(prompt, callback, onToolEvent, onUsage, attachme
|
|
|
303
369
|
const session = await ensureOrchestratorSession();
|
|
304
370
|
// Wait for any in-flight context recovery injection to finish before sending
|
|
305
371
|
if (recoveryInjectionPromise) {
|
|
306
|
-
console.log(
|
|
307
|
-
await recoveryInjectionPromise
|
|
372
|
+
console.log("[nzb] Waiting for context recovery…");
|
|
373
|
+
await withTimeout(recoveryInjectionPromise, 25_000, "recovery injection wait").catch(() => {
|
|
374
|
+
console.log("[nzb] Recovery injection wait timed out, proceeding anyway");
|
|
375
|
+
});
|
|
308
376
|
}
|
|
309
377
|
currentCallback = callback;
|
|
310
378
|
let accumulated = "";
|
|
311
379
|
let toolCallExecuted = false;
|
|
312
380
|
const unsubToolStart = session.on("tool.execution_start", (event) => {
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
args?.
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
381
|
+
try {
|
|
382
|
+
const toolName = event?.data?.toolName || event?.data?.name || "tool";
|
|
383
|
+
const args = event?.data?.arguments;
|
|
384
|
+
const detail = args?.description ||
|
|
385
|
+
args?.command?.slice(0, 80) ||
|
|
386
|
+
args?.intent ||
|
|
387
|
+
args?.pattern ||
|
|
388
|
+
args?.prompt?.slice(0, 80) ||
|
|
389
|
+
undefined;
|
|
390
|
+
onToolEvent?.({ type: "tool_start", toolName, detail });
|
|
391
|
+
}
|
|
392
|
+
catch (err) {
|
|
393
|
+
console.error("[nzb] Error in tool.execution_start listener:", err instanceof Error ? err.message : err);
|
|
394
|
+
}
|
|
322
395
|
});
|
|
323
396
|
const unsubToolDone = session.on("tool.execution_complete", (event) => {
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
397
|
+
try {
|
|
398
|
+
toolCallExecuted = true;
|
|
399
|
+
const toolName = event?.data?.toolName || event?.data?.name || "tool";
|
|
400
|
+
onToolEvent?.({ type: "tool_complete", toolName });
|
|
401
|
+
}
|
|
402
|
+
catch (err) {
|
|
403
|
+
console.error("[nzb] Error in tool.execution_complete listener:", err instanceof Error ? err.message : err);
|
|
404
|
+
}
|
|
327
405
|
});
|
|
328
406
|
const unsubDelta = session.on("assistant.message_delta", (event) => {
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
accumulated
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
407
|
+
try {
|
|
408
|
+
// After a tool call completes, ensure a line break separates the text blocks
|
|
409
|
+
// so they don't visually run together in the TUI.
|
|
410
|
+
if (toolCallExecuted && accumulated.length > 0 && !accumulated.endsWith("\n")) {
|
|
411
|
+
accumulated += "\n";
|
|
412
|
+
}
|
|
413
|
+
toolCallExecuted = false;
|
|
414
|
+
accumulated += event.data.deltaContent;
|
|
415
|
+
callback(accumulated, false);
|
|
416
|
+
}
|
|
417
|
+
catch (err) {
|
|
418
|
+
console.error("[nzb] Error in message_delta listener:", err instanceof Error ? err.message : err);
|
|
419
|
+
}
|
|
337
420
|
});
|
|
338
421
|
const unsubError = session.on("session.error", (event) => {
|
|
339
|
-
|
|
340
|
-
|
|
422
|
+
try {
|
|
423
|
+
const errMsg = event?.data?.message || event?.data?.error || "Unknown session error";
|
|
424
|
+
console.error(`[nzb] Session error event: ${errMsg}`);
|
|
425
|
+
}
|
|
426
|
+
catch (err) {
|
|
427
|
+
console.error("[nzb] Error in session.error listener:", err instanceof Error ? err.message : err);
|
|
428
|
+
}
|
|
341
429
|
});
|
|
342
430
|
const unsubPartialResult = session.on("tool.execution_partial_result", (event) => {
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
431
|
+
try {
|
|
432
|
+
const toolName = event?.data?.toolName || event?.data?.name || "tool";
|
|
433
|
+
const partialOutput = event?.data?.partialOutput || "";
|
|
434
|
+
onToolEvent?.({ type: "tool_partial_result", toolName, detail: partialOutput });
|
|
435
|
+
}
|
|
436
|
+
catch (err) {
|
|
437
|
+
console.error("[nzb] Error in tool.execution_partial_result listener:", err instanceof Error ? err.message : err);
|
|
438
|
+
}
|
|
346
439
|
});
|
|
347
440
|
const unsubUsage = session.on("assistant.usage", (event) => {
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
441
|
+
try {
|
|
442
|
+
const inputTokens = event?.data?.inputTokens || 0;
|
|
443
|
+
const outputTokens = event?.data?.outputTokens || 0;
|
|
444
|
+
const model = event?.data?.model || undefined;
|
|
445
|
+
const duration = event?.data?.duration || undefined;
|
|
446
|
+
onUsage?.({ inputTokens, outputTokens, model, duration });
|
|
447
|
+
}
|
|
448
|
+
catch (err) {
|
|
449
|
+
console.error("[nzb] Error in assistant.usage listener:", err instanceof Error ? err.message : err);
|
|
450
|
+
}
|
|
353
451
|
});
|
|
354
452
|
try {
|
|
355
453
|
const sendPayload = { prompt };
|
|
@@ -364,6 +462,16 @@ async function executeOnSession(prompt, callback, onToolEvent, onUsage, attachme
|
|
|
364
462
|
}
|
|
365
463
|
catch (err) {
|
|
366
464
|
const msg = err instanceof Error ? err.message : String(err);
|
|
465
|
+
// Vision not supported — the session is now tainted with image data in its history,
|
|
466
|
+
// so ALL subsequent messages would fail. Force-recreate the session to recover.
|
|
467
|
+
// The retry (with stripped attachments) is handled by sendToOrchestrator's retry loop.
|
|
468
|
+
if (/not supported for vision/i.test(msg)) {
|
|
469
|
+
console.log(`[nzb] Model '${config.copilotModel}' does not support vision — destroying tainted session`);
|
|
470
|
+
orchestratorSession = undefined;
|
|
471
|
+
sessionCreatedAt = undefined;
|
|
472
|
+
deleteState(ORCHESTRATOR_SESSION_KEY);
|
|
473
|
+
throw err;
|
|
474
|
+
}
|
|
367
475
|
// On timeout, deliver whatever was accumulated instead of retrying from scratch
|
|
368
476
|
if (/timeout/i.test(msg) && accumulated.length > 0) {
|
|
369
477
|
console.log(`[nzb] Timeout — delivering ${accumulated.length} chars of partial content`);
|
|
@@ -373,6 +481,7 @@ async function executeOnSession(prompt, callback, onToolEvent, onUsage, attachme
|
|
|
373
481
|
if (/closed|destroy|disposed|invalid|expired|not found/i.test(msg)) {
|
|
374
482
|
console.log(`[nzb] Session appears dead, will recreate: ${msg}`);
|
|
375
483
|
orchestratorSession = undefined;
|
|
484
|
+
sessionCreatedAt = undefined;
|
|
376
485
|
deleteState(ORCHESTRATOR_SESSION_KEY);
|
|
377
486
|
}
|
|
378
487
|
throw err;
|
|
@@ -396,19 +505,30 @@ async function processQueue() {
|
|
|
396
505
|
return;
|
|
397
506
|
}
|
|
398
507
|
processing = true;
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
508
|
+
try {
|
|
509
|
+
while (messageQueue.length > 0) {
|
|
510
|
+
const item = messageQueue.shift();
|
|
511
|
+
currentSourceChannel = item.sourceChannel;
|
|
512
|
+
try {
|
|
513
|
+
const result = await executeOnSession(item.prompt, item.callback, item.onToolEvent, item.onUsage, item.attachments);
|
|
514
|
+
item.resolve(result);
|
|
515
|
+
}
|
|
516
|
+
catch (err) {
|
|
517
|
+
item.reject(err);
|
|
518
|
+
}
|
|
519
|
+
currentSourceChannel = undefined;
|
|
408
520
|
}
|
|
409
|
-
currentSourceChannel = undefined;
|
|
410
521
|
}
|
|
411
|
-
|
|
522
|
+
finally {
|
|
523
|
+
processing = false;
|
|
524
|
+
}
|
|
525
|
+
// Re-check for messages that arrived during the last executeOnSession call
|
|
526
|
+
if (messageQueue.length > 0) {
|
|
527
|
+
processQueue().catch((err) => {
|
|
528
|
+
console.error("[nzb] processQueue re-check failed:", err instanceof Error ? err.message : err);
|
|
529
|
+
processing = false;
|
|
530
|
+
});
|
|
531
|
+
}
|
|
412
532
|
}
|
|
413
533
|
function isRecoverableError(err) {
|
|
414
534
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -445,94 +565,137 @@ export async function sendToOrchestrator(prompt, source, callback, onToolEvent,
|
|
|
445
565
|
const sourceChannel = source.type === "telegram" ? "telegram" : source.type === "tui" ? "tui" : undefined;
|
|
446
566
|
// Enqueue with priority — user messages go before background messages
|
|
447
567
|
void (async () => {
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
568
|
+
// Safety timeout for entire message processing chain.
|
|
569
|
+
// Uses a flag to prevent double-callback if timeout fires while processing completes.
|
|
570
|
+
const GLOBAL_MSG_TIMEOUT_MS = 300_000; // 5 minutes
|
|
571
|
+
let globalTimedOut = false;
|
|
572
|
+
const globalTimer = setTimeout(() => {
|
|
573
|
+
globalTimedOut = true;
|
|
574
|
+
console.error("[nzb] Global message processing timeout (5 min). Force-failing.");
|
|
575
|
+
Promise.resolve(callback("Error: Message processing timed out after 5 minutes. Please try again.", true)).catch(() => { });
|
|
576
|
+
}, GLOBAL_MSG_TIMEOUT_MS);
|
|
577
|
+
try {
|
|
578
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
579
|
+
try {
|
|
580
|
+
const finalContent = await new Promise((resolve, reject) => {
|
|
581
|
+
const item = {
|
|
582
|
+
prompt: taggedPrompt,
|
|
583
|
+
attachments,
|
|
584
|
+
callback,
|
|
585
|
+
onToolEvent,
|
|
586
|
+
onUsage,
|
|
587
|
+
sourceChannel,
|
|
588
|
+
resolve,
|
|
589
|
+
reject,
|
|
590
|
+
};
|
|
591
|
+
if (source.type === "background") {
|
|
592
|
+
// Background results go to the back of the queue
|
|
593
|
+
messageQueue.push(item);
|
|
470
594
|
}
|
|
471
595
|
else {
|
|
472
|
-
|
|
596
|
+
// User messages inserted before any background messages (priority)
|
|
597
|
+
const bgIndex = messageQueue.findIndex(isBackgroundMessage);
|
|
598
|
+
if (bgIndex >= 0) {
|
|
599
|
+
messageQueue.splice(bgIndex, 0, item);
|
|
600
|
+
}
|
|
601
|
+
else {
|
|
602
|
+
messageQueue.push(item);
|
|
603
|
+
}
|
|
473
604
|
}
|
|
605
|
+
processQueue();
|
|
606
|
+
});
|
|
607
|
+
// Deliver response to user FIRST, then log best-effort
|
|
608
|
+
try {
|
|
609
|
+
logMessage("out", sourceLabel, finalContent);
|
|
474
610
|
}
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
}
|
|
481
|
-
catch {
|
|
482
|
-
/* best-effort */
|
|
483
|
-
}
|
|
484
|
-
// Log both sides of the conversation before delivery so we have the row ID
|
|
485
|
-
let assistantLogId;
|
|
486
|
-
try {
|
|
487
|
-
const telegramMsgId = source.type === "telegram" ? source.messageId : undefined;
|
|
488
|
-
logConversation(logRole, prompt, sourceLabel, telegramMsgId);
|
|
489
|
-
}
|
|
490
|
-
catch {
|
|
491
|
-
/* best-effort */
|
|
492
|
-
}
|
|
493
|
-
try {
|
|
494
|
-
assistantLogId = logConversation("assistant", finalContent, sourceLabel);
|
|
495
|
-
}
|
|
496
|
-
catch {
|
|
497
|
-
/* best-effort */
|
|
498
|
-
}
|
|
499
|
-
await callback(finalContent, true, { assistantLogId });
|
|
500
|
-
// Auto-continue: if the response was cut short by timeout, automatically
|
|
501
|
-
// send a follow-up "Continue" message so the user doesn't have to
|
|
502
|
-
if (finalContent.includes("⏱ Response was cut short (timeout)") && _autoContinueCount < MAX_AUTO_CONTINUE) {
|
|
503
|
-
console.log(`[nzb] Auto-continuing after timeout (${_autoContinueCount + 1}/${MAX_AUTO_CONTINUE})…`);
|
|
504
|
-
await sleep(1000);
|
|
505
|
-
void sendToOrchestrator("Continue from where you left off. Do not repeat what was already said.", source, callback, onToolEvent, onUsage, _autoContinueCount + 1);
|
|
506
|
-
}
|
|
507
|
-
return;
|
|
508
|
-
}
|
|
509
|
-
catch (err) {
|
|
510
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
511
|
-
// Don't retry cancelled messages
|
|
512
|
-
if (/cancelled|abort/i.test(msg)) {
|
|
513
|
-
return;
|
|
514
|
-
}
|
|
515
|
-
if (isRecoverableError(err) && attempt < MAX_RETRIES) {
|
|
516
|
-
const delay = RECONNECT_DELAYS_MS[Math.min(attempt, RECONNECT_DELAYS_MS.length - 1)];
|
|
517
|
-
console.error(`[nzb] Recoverable error: ${msg}. Retry ${attempt + 1}/${MAX_RETRIES} after ${delay}ms…`);
|
|
518
|
-
await sleep(delay);
|
|
519
|
-
// Reset client before retry in case the connection is stale
|
|
611
|
+
catch {
|
|
612
|
+
/* best-effort */
|
|
613
|
+
}
|
|
614
|
+
// Log both sides of the conversation before delivery so we have the row ID
|
|
615
|
+
let assistantLogId;
|
|
520
616
|
try {
|
|
521
|
-
|
|
617
|
+
const telegramMsgId = source.type === "telegram" ? source.messageId : undefined;
|
|
618
|
+
logConversation(logRole, prompt, sourceLabel, telegramMsgId);
|
|
522
619
|
}
|
|
523
620
|
catch {
|
|
524
|
-
/*
|
|
621
|
+
/* best-effort */
|
|
622
|
+
}
|
|
623
|
+
try {
|
|
624
|
+
assistantLogId = logConversation("assistant", finalContent, sourceLabel);
|
|
625
|
+
}
|
|
626
|
+
catch {
|
|
627
|
+
/* best-effort */
|
|
628
|
+
}
|
|
629
|
+
try {
|
|
630
|
+
if (!globalTimedOut) {
|
|
631
|
+
await callback(finalContent, true, { assistantLogId });
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
catch (callbackErr) {
|
|
635
|
+
console.error("[nzb] Callback error after successful response:", callbackErr instanceof Error ? callbackErr.message : callbackErr);
|
|
525
636
|
}
|
|
526
|
-
continue
|
|
637
|
+
// Auto-continue: if the response was cut short by timeout, automatically
|
|
638
|
+
// send a follow-up "Continue" message so the user doesn't have to
|
|
639
|
+
if (finalContent.includes("⏱ Response was cut short (timeout)") && _autoContinueCount < MAX_AUTO_CONTINUE) {
|
|
640
|
+
console.log(`[nzb] Auto-continuing after timeout (${_autoContinueCount + 1}/${MAX_AUTO_CONTINUE})…`);
|
|
641
|
+
await sleep(1000);
|
|
642
|
+
void sendToOrchestrator("Continue from where you left off. Do not repeat what was already said.", source, callback, onToolEvent, onUsage, _autoContinueCount + 1);
|
|
643
|
+
}
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
catch (err) {
|
|
647
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
648
|
+
// Don't retry cancelled messages
|
|
649
|
+
if (/cancelled|abort/i.test(msg)) {
|
|
650
|
+
if (!globalTimedOut) {
|
|
651
|
+
try {
|
|
652
|
+
await callback("Request was cancelled.", true);
|
|
653
|
+
}
|
|
654
|
+
catch { /* best-effort */ }
|
|
655
|
+
}
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
// Vision not supported — strip attachments and retry with text-only prompt.
|
|
659
|
+
// executeOnSession already destroyed the tainted session.
|
|
660
|
+
if (/not supported for vision/i.test(msg)) {
|
|
661
|
+
console.log(`[nzb] Vision not supported — retrying without attachments`);
|
|
662
|
+
attachments = undefined;
|
|
663
|
+
taggedPrompt =
|
|
664
|
+
`[System: The current model '${config.copilotModel}' does not support image/vision analysis. ` +
|
|
665
|
+
`The image path is already included in the user's message below. ` +
|
|
666
|
+
`Please inform the user that the current model doesn't support direct image analysis, ` +
|
|
667
|
+
`and suggest switching to a vision-capable model (e.g. gpt-4o, claude-sonnet-4, gemini-2.0-flash) ` +
|
|
668
|
+
`using the /model command.]\n\n${taggedPrompt}`;
|
|
669
|
+
continue;
|
|
670
|
+
}
|
|
671
|
+
if (isRecoverableError(err) && attempt < MAX_RETRIES) {
|
|
672
|
+
const delay = RECONNECT_DELAYS_MS[Math.min(attempt, RECONNECT_DELAYS_MS.length - 1)];
|
|
673
|
+
console.error(`[nzb] Recoverable error: ${msg}. Retry ${attempt + 1}/${MAX_RETRIES} after ${delay}ms…`);
|
|
674
|
+
await sleep(delay);
|
|
675
|
+
// Reset client before retry in case the connection is stale
|
|
676
|
+
try {
|
|
677
|
+
await ensureClient();
|
|
678
|
+
}
|
|
679
|
+
catch {
|
|
680
|
+
/* will fail again on next attempt */
|
|
681
|
+
}
|
|
682
|
+
continue;
|
|
683
|
+
}
|
|
684
|
+
console.error(`[nzb] Error processing message: ${msg}`);
|
|
685
|
+
if (!globalTimedOut) {
|
|
686
|
+
await callback(`Error: ${msg}`, true);
|
|
687
|
+
}
|
|
688
|
+
return;
|
|
527
689
|
}
|
|
528
|
-
console.error(`[nzb] Error processing message: ${msg}`);
|
|
529
|
-
await callback(`Error: ${msg}`, true);
|
|
530
|
-
return;
|
|
531
690
|
}
|
|
532
691
|
}
|
|
533
|
-
|
|
692
|
+
finally {
|
|
693
|
+
clearTimeout(globalTimer);
|
|
694
|
+
}
|
|
695
|
+
})().catch((err) => {
|
|
696
|
+
console.error(`[nzb] Unhandled error in sendToOrchestrator: ${err instanceof Error ? err.message : String(err)}`);
|
|
697
|
+
});
|
|
534
698
|
}
|
|
535
|
-
/** Cancel the in-flight message and drain the queue. */
|
|
536
699
|
export async function cancelCurrentMessage() {
|
|
537
700
|
// Drain any queued messages
|
|
538
701
|
const drained = messageQueue.length;
|
|
@@ -583,6 +746,7 @@ export async function resetSession() {
|
|
|
583
746
|
}
|
|
584
747
|
catch { }
|
|
585
748
|
orchestratorSession = undefined;
|
|
749
|
+
sessionCreatedAt = undefined;
|
|
586
750
|
}
|
|
587
751
|
// Clear persisted session ID so a fresh session is created
|
|
588
752
|
deleteState(ORCHESTRATOR_SESSION_KEY);
|
package/dist/copilot/skills.js
CHANGED
|
@@ -41,7 +41,8 @@ export function listSkills() {
|
|
|
41
41
|
try {
|
|
42
42
|
entries = readdirSync(dir);
|
|
43
43
|
}
|
|
44
|
-
catch {
|
|
44
|
+
catch (err) {
|
|
45
|
+
console.error("[nzb] Failed to read skill directory", dir + ":", err instanceof Error ? err.message : err);
|
|
45
46
|
continue;
|
|
46
47
|
}
|
|
47
48
|
for (const entry of entries) {
|
|
@@ -60,7 +61,8 @@ export function listSkills() {
|
|
|
60
61
|
source,
|
|
61
62
|
});
|
|
62
63
|
}
|
|
63
|
-
catch {
|
|
64
|
+
catch (err) {
|
|
65
|
+
console.error("[nzb] Failed to parse SKILL.md for", entry + ":", err instanceof Error ? err.message : err);
|
|
64
66
|
skills.push({
|
|
65
67
|
slug: entry,
|
|
66
68
|
name: entry,
|