@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
package/dist/copilot/tools.js
CHANGED
|
@@ -5,8 +5,9 @@ import { join, resolve, sep } from "path";
|
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
import { config, persistModel } from "../config.js";
|
|
7
7
|
import { SESSIONS_DIR } from "../paths.js";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
8
|
+
import { getDb } from "../store/db.js";
|
|
9
|
+
import { withTimeout } from "../utils.js";
|
|
10
|
+
import { addMemory, removeMemory, searchMemories } from "../store/memory.js";
|
|
10
11
|
import { createSkill, listSkills, removeSkill } from "./skills.js";
|
|
11
12
|
function isTimeoutError(err) {
|
|
12
13
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -79,7 +80,7 @@ export function createTools(deps) {
|
|
|
79
80
|
session,
|
|
80
81
|
workingDir: args.working_dir,
|
|
81
82
|
status: "idle",
|
|
82
|
-
originChannel: getCurrentSourceChannel(),
|
|
83
|
+
originChannel: deps.getCurrentSourceChannel(),
|
|
83
84
|
};
|
|
84
85
|
deps.workers.set(args.name, worker);
|
|
85
86
|
deps.onWorkerEvent?.({ type: "created", name: args.name, workingDir: args.working_dir });
|
|
@@ -111,7 +112,9 @@ export function createTools(deps) {
|
|
|
111
112
|
})
|
|
112
113
|
.finally(() => {
|
|
113
114
|
// Auto-destroy background workers after completion to free memory (~400MB per worker)
|
|
114
|
-
session.disconnect().catch(() => {
|
|
115
|
+
withTimeout(session.disconnect(), 5_000, `worker '${args.name}' disconnect`).catch((err) => {
|
|
116
|
+
console.error(`[nzb] Worker '${args.name}' disconnect timeout/error:`, err instanceof Error ? err.message : err);
|
|
117
|
+
});
|
|
115
118
|
deps.workers.delete(args.name);
|
|
116
119
|
try {
|
|
117
120
|
getDb().prepare(`DELETE FROM worker_sessions WHERE name = ?`).run(args.name);
|
|
@@ -162,7 +165,9 @@ export function createTools(deps) {
|
|
|
162
165
|
})
|
|
163
166
|
.finally(() => {
|
|
164
167
|
// Auto-destroy after each send_to_worker dispatch to free memory
|
|
165
|
-
worker.session.disconnect().catch(() => {
|
|
168
|
+
withTimeout(worker.session.disconnect(), 5_000, `worker '${args.name}' disconnect`).catch((err) => {
|
|
169
|
+
console.error(`[nzb] Worker '${args.name}' disconnect timeout/error:`, err instanceof Error ? err.message : err);
|
|
170
|
+
});
|
|
166
171
|
deps.workers.delete(args.name);
|
|
167
172
|
try {
|
|
168
173
|
getDb().prepare(`DELETE FROM worker_sessions WHERE name = ?`).run(args.name);
|
|
@@ -210,10 +215,10 @@ export function createTools(deps) {
|
|
|
210
215
|
return `No worker named '${args.name}'.`;
|
|
211
216
|
}
|
|
212
217
|
try {
|
|
213
|
-
await worker.session.disconnect();
|
|
218
|
+
await withTimeout(worker.session.disconnect(), 5_000, `worker '${args.name}' disconnect`);
|
|
214
219
|
}
|
|
215
|
-
catch {
|
|
216
|
-
|
|
220
|
+
catch (err) {
|
|
221
|
+
console.error(`[nzb] Worker '${args.name}' disconnect timeout/error:`, err instanceof Error ? err.message : err);
|
|
217
222
|
}
|
|
218
223
|
deps.workers.delete(args.name);
|
|
219
224
|
const db = getDb();
|
|
@@ -221,6 +226,36 @@ export function createTools(deps) {
|
|
|
221
226
|
return `Worker '${args.name}' terminated.`;
|
|
222
227
|
},
|
|
223
228
|
}),
|
|
229
|
+
defineTool("kill_worker", {
|
|
230
|
+
description: "Force-kill a stuck or unresponsive worker session by name. " +
|
|
231
|
+
"Use when a worker is hanging or no longer needed.",
|
|
232
|
+
parameters: z.object({
|
|
233
|
+
name: z.string().describe("Name of the worker to kill"),
|
|
234
|
+
}),
|
|
235
|
+
handler: async (args) => {
|
|
236
|
+
const worker = deps.workers.get(args.name);
|
|
237
|
+
if (!worker)
|
|
238
|
+
return `No worker found with name '${args.name}'.`;
|
|
239
|
+
try {
|
|
240
|
+
withTimeout(worker.session.disconnect(), 5_000, `worker '${args.name}' disconnect`).catch((err) => {
|
|
241
|
+
console.error(`[nzb] Worker '${args.name}' disconnect timeout/error:`, err instanceof Error ? err.message : err);
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
catch {
|
|
245
|
+
// Session may already be destroyed
|
|
246
|
+
}
|
|
247
|
+
deps.workers.delete(args.name);
|
|
248
|
+
try {
|
|
249
|
+
getDb()
|
|
250
|
+
.prepare(`DELETE FROM worker_sessions WHERE name = ?`)
|
|
251
|
+
.run(args.name);
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
// DB cleanup is best-effort
|
|
255
|
+
}
|
|
256
|
+
return `Worker '${args.name}' force-killed.`;
|
|
257
|
+
},
|
|
258
|
+
}),
|
|
224
259
|
// ── Agent Team Tools ──────────────────────────────────────────
|
|
225
260
|
defineTool("create_agent_team", {
|
|
226
261
|
description: "Create an agent team — multiple workers collaborating on a task in parallel. Each member gets a role " +
|
|
@@ -264,7 +299,7 @@ export function createTools(deps) {
|
|
|
264
299
|
}
|
|
265
300
|
}
|
|
266
301
|
const teamId = args.team_name;
|
|
267
|
-
const originChannel = getCurrentSourceChannel();
|
|
302
|
+
const originChannel = deps.getCurrentSourceChannel();
|
|
268
303
|
const { createTeam: dbCreateTeam, addTeamMember: dbAddTeamMember } = await import("../store/db.js");
|
|
269
304
|
dbCreateTeam(teamId, args.task_description, originChannel);
|
|
270
305
|
const teamInfo = {
|
|
@@ -319,7 +354,9 @@ export function createTools(deps) {
|
|
|
319
354
|
deps.onWorkerComplete(member.name, errMsg);
|
|
320
355
|
})
|
|
321
356
|
.finally(() => {
|
|
322
|
-
session.disconnect().catch(() => {
|
|
357
|
+
withTimeout(session.disconnect(), 5_000, `worker '${member.name}' disconnect`).catch((err) => {
|
|
358
|
+
console.error(`[nzb] Worker '${member.name}' disconnect timeout/error:`, err instanceof Error ? err.message : err);
|
|
359
|
+
});
|
|
323
360
|
deps.workers.delete(member.name);
|
|
324
361
|
try {
|
|
325
362
|
getDb().prepare(`DELETE FROM worker_sessions WHERE name = ?`).run(member.name);
|
|
@@ -488,7 +525,7 @@ export function createTools(deps) {
|
|
|
488
525
|
session,
|
|
489
526
|
workingDir: "(attached)",
|
|
490
527
|
status: "idle",
|
|
491
|
-
originChannel: getCurrentSourceChannel(),
|
|
528
|
+
originChannel: deps.getCurrentSourceChannel(),
|
|
492
529
|
};
|
|
493
530
|
deps.workers.set(args.name, worker);
|
|
494
531
|
const db = getDb();
|
package/dist/daemon.js
CHANGED
|
@@ -26,6 +26,7 @@ function isProcessAlive(pid) {
|
|
|
26
26
|
return true;
|
|
27
27
|
}
|
|
28
28
|
catch {
|
|
29
|
+
// Expected: process.kill(0) throws when process doesn't exist
|
|
29
30
|
return false;
|
|
30
31
|
}
|
|
31
32
|
}
|
|
@@ -64,8 +65,8 @@ function releasePidLock() {
|
|
|
64
65
|
}
|
|
65
66
|
}
|
|
66
67
|
}
|
|
67
|
-
catch {
|
|
68
|
-
|
|
68
|
+
catch (err) {
|
|
69
|
+
console.error("[nzb] PID lock cleanup:", err instanceof Error ? err.message : err);
|
|
69
70
|
}
|
|
70
71
|
}
|
|
71
72
|
async function main() {
|
|
@@ -197,8 +198,8 @@ async function shutdown() {
|
|
|
197
198
|
try {
|
|
198
199
|
await stopBot();
|
|
199
200
|
}
|
|
200
|
-
catch {
|
|
201
|
-
|
|
201
|
+
catch (err) {
|
|
202
|
+
console.error("[nzb] stopBot during shutdown:", err instanceof Error ? err.message : err);
|
|
202
203
|
}
|
|
203
204
|
}
|
|
204
205
|
// Destroy all active worker sessions to free memory
|
|
@@ -207,8 +208,8 @@ async function shutdown() {
|
|
|
207
208
|
try {
|
|
208
209
|
await stopClient();
|
|
209
210
|
}
|
|
210
|
-
catch {
|
|
211
|
-
|
|
211
|
+
catch (err) {
|
|
212
|
+
console.error("[nzb] stopClient during shutdown:", err instanceof Error ? err.message : err);
|
|
212
213
|
}
|
|
213
214
|
closeDb();
|
|
214
215
|
releasePidLock();
|
|
@@ -229,8 +230,8 @@ export async function restartDaemon() {
|
|
|
229
230
|
try {
|
|
230
231
|
await stopBot();
|
|
231
232
|
}
|
|
232
|
-
catch {
|
|
233
|
-
|
|
233
|
+
catch (err) {
|
|
234
|
+
console.error("[nzb] stopBot during restart:", err instanceof Error ? err.message : err);
|
|
234
235
|
}
|
|
235
236
|
}
|
|
236
237
|
// Destroy all active worker sessions to free memory
|
|
@@ -239,8 +240,8 @@ export async function restartDaemon() {
|
|
|
239
240
|
try {
|
|
240
241
|
await stopClient();
|
|
241
242
|
}
|
|
242
|
-
catch {
|
|
243
|
-
|
|
243
|
+
catch (err) {
|
|
244
|
+
console.error("[nzb] stopClient during restart:", err instanceof Error ? err.message : err);
|
|
244
245
|
}
|
|
245
246
|
closeDb();
|
|
246
247
|
releasePidLock();
|
package/dist/setup.js
CHANGED
|
@@ -28,14 +28,15 @@ async function fetchModels() {
|
|
|
28
28
|
});
|
|
29
29
|
}
|
|
30
30
|
catch {
|
|
31
|
+
// Expected: Copilot CLI may not be authenticated yet
|
|
31
32
|
return [];
|
|
32
33
|
}
|
|
33
34
|
finally {
|
|
34
35
|
try {
|
|
35
36
|
await client?.stop();
|
|
36
37
|
}
|
|
37
|
-
catch {
|
|
38
|
-
|
|
38
|
+
catch (err) {
|
|
39
|
+
console.error("[nzb] CopilotClient stop:", err instanceof Error ? err.message : err);
|
|
39
40
|
}
|
|
40
41
|
}
|
|
41
42
|
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { getDb } from "./db.js";
|
|
2
|
+
// Lazy per-connection prepared statement cache
|
|
3
|
+
let cachedDb;
|
|
4
|
+
let stmtCache;
|
|
5
|
+
function ensureStmtCache() {
|
|
6
|
+
const db = getDb();
|
|
7
|
+
if (db !== cachedDb) {
|
|
8
|
+
cachedDb = db;
|
|
9
|
+
stmtCache = {
|
|
10
|
+
logConversation: db.prepare(`INSERT INTO conversation_log (role, content, source, telegram_msg_id) VALUES (?, ?, ?, ?)`),
|
|
11
|
+
pruneConversation: db.prepare(`DELETE FROM conversation_log WHERE id NOT IN (SELECT id FROM conversation_log ORDER BY id DESC LIMIT 200)`),
|
|
12
|
+
getConversationByMsgId: db.prepare(`SELECT id FROM conversation_log WHERE telegram_msg_id = ? LIMIT 1`),
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
return stmtCache;
|
|
16
|
+
}
|
|
17
|
+
/** Log a conversation turn (user, assistant, or system) with optional Telegram message ID. Returns the row ID. */
|
|
18
|
+
export function logConversation(role, content, source, telegramMsgId) {
|
|
19
|
+
const cache = ensureStmtCache();
|
|
20
|
+
const result = cache.logConversation.run(role, content, source, telegramMsgId ?? null);
|
|
21
|
+
// Prune every ~50 inserts using rowid (crash-safe, no in-memory counter)
|
|
22
|
+
const rowId = result.lastInsertRowid;
|
|
23
|
+
if (rowId % 50 === 0) {
|
|
24
|
+
try {
|
|
25
|
+
cache.pruneConversation.run();
|
|
26
|
+
}
|
|
27
|
+
catch (err) {
|
|
28
|
+
console.error("[nzb] Conversation prune failed:", err instanceof Error ? err.message : err);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return rowId;
|
|
32
|
+
}
|
|
33
|
+
/** Get conversation context around a Telegram message ID (±4 rows using proper subquery). */
|
|
34
|
+
export function getConversationContext(telegramMsgId) {
|
|
35
|
+
const db = getDb();
|
|
36
|
+
const cache = ensureStmtCache();
|
|
37
|
+
const row = cache.getConversationByMsgId.get(telegramMsgId);
|
|
38
|
+
if (!row)
|
|
39
|
+
return undefined;
|
|
40
|
+
// Fetch 4 rows before + the target + 4 rows after (handles ID gaps from pruning)
|
|
41
|
+
const rows = db
|
|
42
|
+
.prepare(`
|
|
43
|
+
SELECT role, content, source, ts FROM (
|
|
44
|
+
SELECT * FROM conversation_log WHERE id < ? ORDER BY id DESC LIMIT 4
|
|
45
|
+
)
|
|
46
|
+
UNION ALL
|
|
47
|
+
SELECT role, content, source, ts FROM conversation_log WHERE id = ?
|
|
48
|
+
UNION ALL
|
|
49
|
+
SELECT role, content, source, ts FROM (
|
|
50
|
+
SELECT * FROM conversation_log WHERE id > ? ORDER BY id ASC LIMIT 4
|
|
51
|
+
)
|
|
52
|
+
`)
|
|
53
|
+
.all(row.id, row.id, row.id);
|
|
54
|
+
if (rows.length === 0)
|
|
55
|
+
return undefined;
|
|
56
|
+
return rows
|
|
57
|
+
.map((r) => {
|
|
58
|
+
const tag = r.role === "user" ? "You" : r.role === "assistant" ? "NZB" : "System";
|
|
59
|
+
const content = r.content.length > 400 ? r.content.slice(0, 400) + "…" : r.content;
|
|
60
|
+
return `${tag}: ${content}`;
|
|
61
|
+
})
|
|
62
|
+
.join("\n");
|
|
63
|
+
}
|
|
64
|
+
/** Set Telegram message ID on a specific conversation_log row (race-free). */
|
|
65
|
+
export function setConversationTelegramMsgId(rowId, telegramMsgId) {
|
|
66
|
+
const db = getDb();
|
|
67
|
+
db.prepare(`UPDATE conversation_log SET telegram_msg_id = ? WHERE id = ?`).run(telegramMsgId, rowId);
|
|
68
|
+
}
|
|
69
|
+
/** Look up conversation content by Telegram message ID. Returns the message content or undefined. */
|
|
70
|
+
export function getConversationByTelegramMsgId(telegramMsgId) {
|
|
71
|
+
const db = getDb();
|
|
72
|
+
const row = db
|
|
73
|
+
.prepare(`SELECT content FROM conversation_log WHERE telegram_msg_id = ? LIMIT 1`)
|
|
74
|
+
.get(telegramMsgId);
|
|
75
|
+
return row?.content;
|
|
76
|
+
}
|
|
77
|
+
/** Get recent conversation history formatted for injection into system message. */
|
|
78
|
+
export function getRecentConversation(limit = 20) {
|
|
79
|
+
const db = getDb();
|
|
80
|
+
const rows = db
|
|
81
|
+
.prepare(`SELECT role, content, source, ts FROM conversation_log ORDER BY id DESC LIMIT ?`)
|
|
82
|
+
.all(limit);
|
|
83
|
+
if (rows.length === 0)
|
|
84
|
+
return "";
|
|
85
|
+
// Reverse so oldest is first (chronological order)
|
|
86
|
+
rows.reverse();
|
|
87
|
+
return rows
|
|
88
|
+
.map((r) => {
|
|
89
|
+
const tag = r.role === "user" ? `[${r.source}] User` : r.role === "system" ? `[${r.source}] System` : "NZB";
|
|
90
|
+
// Truncate long messages to keep context manageable
|
|
91
|
+
const content = r.content.length > 500 ? r.content.slice(0, 500) + "…" : r.content;
|
|
92
|
+
return `${tag}: ${content}`;
|
|
93
|
+
})
|
|
94
|
+
.join("\n\n");
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=conversation.js.map
|
package/dist/store/db.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import Database from "better-sqlite3";
|
|
2
2
|
import { DB_PATH, ensureNZBHome } from "../paths.js";
|
|
3
3
|
let db;
|
|
4
|
-
|
|
5
|
-
// Cached prepared statements for hot-path queries (created lazily after DB init)
|
|
4
|
+
// Cached prepared statements for state operations (created lazily after DB init)
|
|
6
5
|
let stmtCache;
|
|
7
6
|
export function getDb() {
|
|
8
7
|
if (!db) {
|
|
9
8
|
ensureNZBHome();
|
|
10
9
|
db = new Database(DB_PATH);
|
|
11
10
|
db.pragma("journal_mode = WAL");
|
|
11
|
+
db.pragma("busy_timeout = 5000");
|
|
12
12
|
db.exec(`
|
|
13
13
|
CREATE TABLE IF NOT EXISTS worker_sessions (
|
|
14
14
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
@@ -115,17 +115,11 @@ export function getDb() {
|
|
|
115
115
|
// FTS5 may not be available — will fall back to LIKE
|
|
116
116
|
console.log("[nzb] FTS5 not available, using LIKE fallback for memory search");
|
|
117
117
|
}
|
|
118
|
-
// Initialize cached prepared statements for
|
|
118
|
+
// Initialize cached prepared statements for state operations
|
|
119
119
|
stmtCache = {
|
|
120
120
|
getState: db.prepare(`SELECT value FROM nzb_state WHERE key = ?`),
|
|
121
121
|
setState: db.prepare(`INSERT OR REPLACE INTO nzb_state (key, value) VALUES (?, ?)`),
|
|
122
122
|
deleteState: db.prepare(`DELETE FROM nzb_state WHERE key = ?`),
|
|
123
|
-
logConversation: db.prepare(`INSERT INTO conversation_log (role, content, source, telegram_msg_id) VALUES (?, ?, ?, ?)`),
|
|
124
|
-
pruneConversation: db.prepare(`DELETE FROM conversation_log WHERE id NOT IN (SELECT id FROM conversation_log ORDER BY id DESC LIMIT 200)`),
|
|
125
|
-
addMemory: db.prepare(`INSERT INTO memories (category, content, source) VALUES (?, ?, ?)`),
|
|
126
|
-
removeMemory: db.prepare(`DELETE FROM memories WHERE id = ?`),
|
|
127
|
-
memorySummary: db.prepare(`SELECT id, category, content FROM memories ORDER BY category, last_accessed DESC`),
|
|
128
|
-
getConversationByMsgId: db.prepare(`SELECT id FROM conversation_log WHERE telegram_msg_id = ? LIMIT 1`),
|
|
129
123
|
};
|
|
130
124
|
}
|
|
131
125
|
return db;
|
|
@@ -144,203 +138,6 @@ export function deleteState(key) {
|
|
|
144
138
|
getDb(); // ensure init
|
|
145
139
|
stmtCache.deleteState.run(key);
|
|
146
140
|
}
|
|
147
|
-
/** Log a conversation turn (user, assistant, or system) with optional Telegram message ID. Returns the row ID. */
|
|
148
|
-
export function logConversation(role, content, source, telegramMsgId) {
|
|
149
|
-
getDb(); // ensure init
|
|
150
|
-
const result = stmtCache.logConversation.run(role, content, source, telegramMsgId ?? null);
|
|
151
|
-
// Keep last 200 entries to support context recovery after session loss
|
|
152
|
-
logInsertCount++;
|
|
153
|
-
if (logInsertCount % 50 === 0) {
|
|
154
|
-
stmtCache.pruneConversation.run();
|
|
155
|
-
}
|
|
156
|
-
return result.lastInsertRowid;
|
|
157
|
-
}
|
|
158
|
-
/** Get conversation context around a Telegram message ID (±4 rows using proper subquery). */
|
|
159
|
-
export function getConversationContext(telegramMsgId) {
|
|
160
|
-
const db = getDb();
|
|
161
|
-
const row = stmtCache.getConversationByMsgId.get(telegramMsgId);
|
|
162
|
-
if (!row)
|
|
163
|
-
return undefined;
|
|
164
|
-
// Fetch 4 rows before + the target + 4 rows after (handles ID gaps from pruning)
|
|
165
|
-
const rows = db
|
|
166
|
-
.prepare(`
|
|
167
|
-
SELECT role, content, source, ts FROM (
|
|
168
|
-
SELECT * FROM conversation_log WHERE id < ? ORDER BY id DESC LIMIT 4
|
|
169
|
-
)
|
|
170
|
-
UNION ALL
|
|
171
|
-
SELECT role, content, source, ts FROM conversation_log WHERE id = ?
|
|
172
|
-
UNION ALL
|
|
173
|
-
SELECT role, content, source, ts FROM (
|
|
174
|
-
SELECT * FROM conversation_log WHERE id > ? ORDER BY id ASC LIMIT 4
|
|
175
|
-
)
|
|
176
|
-
`)
|
|
177
|
-
.all(row.id, row.id, row.id);
|
|
178
|
-
if (rows.length === 0)
|
|
179
|
-
return undefined;
|
|
180
|
-
return rows
|
|
181
|
-
.map((r) => {
|
|
182
|
-
const tag = r.role === "user" ? "You" : r.role === "assistant" ? "NZB" : "System";
|
|
183
|
-
const content = r.content.length > 400 ? r.content.slice(0, 400) + "…" : r.content;
|
|
184
|
-
return `${tag}: ${content}`;
|
|
185
|
-
})
|
|
186
|
-
.join("\n");
|
|
187
|
-
}
|
|
188
|
-
/** Set Telegram message ID on a specific conversation_log row (race-free). */
|
|
189
|
-
export function setConversationTelegramMsgId(rowId, telegramMsgId) {
|
|
190
|
-
const db = getDb();
|
|
191
|
-
db.prepare(`UPDATE conversation_log SET telegram_msg_id = ? WHERE id = ?`).run(telegramMsgId, rowId);
|
|
192
|
-
}
|
|
193
|
-
/** Look up conversation content by Telegram message ID. Returns the message content or undefined. */
|
|
194
|
-
export function getConversationByTelegramMsgId(telegramMsgId) {
|
|
195
|
-
const db = getDb();
|
|
196
|
-
const row = db
|
|
197
|
-
.prepare(`SELECT content FROM conversation_log WHERE telegram_msg_id = ? LIMIT 1`)
|
|
198
|
-
.get(telegramMsgId);
|
|
199
|
-
return row?.content;
|
|
200
|
-
}
|
|
201
|
-
/** Get recent conversation history formatted for injection into system message. */
|
|
202
|
-
export function getRecentConversation(limit = 20) {
|
|
203
|
-
const db = getDb();
|
|
204
|
-
const rows = db
|
|
205
|
-
.prepare(`SELECT role, content, source, ts FROM conversation_log ORDER BY id DESC LIMIT ?`)
|
|
206
|
-
.all(limit);
|
|
207
|
-
if (rows.length === 0)
|
|
208
|
-
return "";
|
|
209
|
-
// Reverse so oldest is first (chronological order)
|
|
210
|
-
rows.reverse();
|
|
211
|
-
return rows
|
|
212
|
-
.map((r) => {
|
|
213
|
-
const tag = r.role === "user" ? `[${r.source}] User` : r.role === "system" ? `[${r.source}] System` : "NZB";
|
|
214
|
-
// Truncate long messages to keep context manageable
|
|
215
|
-
const content = r.content.length > 500 ? r.content.slice(0, 500) + "…" : r.content;
|
|
216
|
-
return `${tag}: ${content}`;
|
|
217
|
-
})
|
|
218
|
-
.join("\n\n");
|
|
219
|
-
}
|
|
220
|
-
/** Add a memory to long-term storage. */
|
|
221
|
-
export function addMemory(category, content, source = "user") {
|
|
222
|
-
getDb(); // ensure init
|
|
223
|
-
const result = stmtCache.addMemory.run(category, content, source);
|
|
224
|
-
return result.lastInsertRowid;
|
|
225
|
-
}
|
|
226
|
-
/** Search memories by keyword and/or category. Uses FTS5 when available, falls back to LIKE. */
|
|
227
|
-
export function searchMemories(keyword, category, limit = 20) {
|
|
228
|
-
const db = getDb();
|
|
229
|
-
// Try FTS5 first for keyword search (much faster than LIKE)
|
|
230
|
-
if (keyword) {
|
|
231
|
-
try {
|
|
232
|
-
const catFilter = category ? `AND m.category = ?` : "";
|
|
233
|
-
const params = [keyword + "*"];
|
|
234
|
-
if (category)
|
|
235
|
-
params.push(category);
|
|
236
|
-
params.push(limit);
|
|
237
|
-
const rows = db
|
|
238
|
-
.prepare(`SELECT m.id, m.category, m.content, m.source, m.created_at
|
|
239
|
-
FROM memories_fts f
|
|
240
|
-
JOIN memories m ON f.rowid = m.id
|
|
241
|
-
WHERE memories_fts MATCH ? ${catFilter}
|
|
242
|
-
ORDER BY rank LIMIT ?`)
|
|
243
|
-
.all(...params);
|
|
244
|
-
return rows;
|
|
245
|
-
}
|
|
246
|
-
catch {
|
|
247
|
-
// FTS5 not available — fall through to LIKE
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
// Fallback: LIKE-based search
|
|
251
|
-
const conditions = [];
|
|
252
|
-
const params = [];
|
|
253
|
-
if (keyword) {
|
|
254
|
-
conditions.push(`content LIKE ?`);
|
|
255
|
-
params.push(`%${keyword}%`);
|
|
256
|
-
}
|
|
257
|
-
if (category) {
|
|
258
|
-
conditions.push(`category = ?`);
|
|
259
|
-
params.push(category);
|
|
260
|
-
}
|
|
261
|
-
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
262
|
-
params.push(limit);
|
|
263
|
-
const rows = db
|
|
264
|
-
.prepare(`SELECT id, category, content, source, created_at FROM memories ${where} ORDER BY last_accessed DESC LIMIT ?`)
|
|
265
|
-
.all(...params);
|
|
266
|
-
// Update last_accessed only when explicitly requested, not on every search
|
|
267
|
-
// (removed automatic last_accessed update to avoid write side effects on reads)
|
|
268
|
-
return rows;
|
|
269
|
-
}
|
|
270
|
-
/** Remove a memory by ID. */
|
|
271
|
-
export function removeMemory(id) {
|
|
272
|
-
getDb(); // ensure init
|
|
273
|
-
const result = stmtCache.removeMemory.run(id);
|
|
274
|
-
return result.changes > 0;
|
|
275
|
-
}
|
|
276
|
-
/** Get a compact summary of all memories for injection into system message. */
|
|
277
|
-
export function getMemorySummary() {
|
|
278
|
-
getDb(); // ensure init
|
|
279
|
-
const rows = stmtCache.memorySummary.all();
|
|
280
|
-
if (rows.length === 0)
|
|
281
|
-
return "";
|
|
282
|
-
// Group by category
|
|
283
|
-
const grouped = {};
|
|
284
|
-
for (const r of rows) {
|
|
285
|
-
if (!grouped[r.category])
|
|
286
|
-
grouped[r.category] = [];
|
|
287
|
-
grouped[r.category].push({ id: r.id, content: r.content });
|
|
288
|
-
}
|
|
289
|
-
const sections = Object.entries(grouped).map(([cat, items]) => {
|
|
290
|
-
const lines = items.map((i) => ` - [#${i.id}] ${i.content}`).join("\n");
|
|
291
|
-
return `**${cat}**:\n${lines}`;
|
|
292
|
-
});
|
|
293
|
-
return sections.join("\n");
|
|
294
|
-
}
|
|
295
|
-
// ── Agent Teams CRUD ──────────────────────────────────────────
|
|
296
|
-
export function createTeam(id, taskDescription, originChannel) {
|
|
297
|
-
const db = getDb();
|
|
298
|
-
db.prepare(`INSERT INTO agent_teams (id, task_description, origin_channel) VALUES (?, ?, ?)`).run(id, taskDescription, originChannel ?? null);
|
|
299
|
-
}
|
|
300
|
-
export function addTeamMember(teamId, workerName, role) {
|
|
301
|
-
const db = getDb();
|
|
302
|
-
db.prepare(`INSERT INTO team_members (team_id, worker_name, role, status) VALUES (?, ?, ?, 'pending')`).run(teamId, workerName, role);
|
|
303
|
-
db.prepare(`UPDATE agent_teams SET member_count = member_count + 1 WHERE id = ?`).run(teamId);
|
|
304
|
-
}
|
|
305
|
-
export function updateTeamMemberResult(teamId, workerName, result, status) {
|
|
306
|
-
const db = getDb();
|
|
307
|
-
db.prepare(`UPDATE team_members SET result = ?, status = ?, completed_at = CURRENT_TIMESTAMP WHERE team_id = ? AND worker_name = ?`).run(result, status, teamId, workerName);
|
|
308
|
-
if (status === "completed" || status === "error") {
|
|
309
|
-
db.prepare(`UPDATE agent_teams SET completed_count = completed_count + 1 WHERE id = ?`).run(teamId);
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
export function getTeam(id) {
|
|
313
|
-
const db = getDb();
|
|
314
|
-
return db.prepare(`SELECT * FROM agent_teams WHERE id = ?`).get(id);
|
|
315
|
-
}
|
|
316
|
-
export function getTeamMembers(teamId) {
|
|
317
|
-
const db = getDb();
|
|
318
|
-
return db
|
|
319
|
-
.prepare(`SELECT worker_name, role, status, result FROM team_members WHERE team_id = ? ORDER BY id`)
|
|
320
|
-
.all(teamId);
|
|
321
|
-
}
|
|
322
|
-
export function completeTeam(teamId, aggregatedResult, status = "completed") {
|
|
323
|
-
const db = getDb();
|
|
324
|
-
db.prepare(`UPDATE agent_teams SET status = ?, aggregated_result = ?, completed_at = CURRENT_TIMESTAMP WHERE id = ?`).run(status, aggregatedResult, teamId);
|
|
325
|
-
}
|
|
326
|
-
export function getActiveTeams() {
|
|
327
|
-
const db = getDb();
|
|
328
|
-
return db
|
|
329
|
-
.prepare(`SELECT id, status, task_description, member_count, completed_count, created_at FROM agent_teams WHERE status = 'active' ORDER BY created_at DESC`)
|
|
330
|
-
.all();
|
|
331
|
-
}
|
|
332
|
-
export function getTeamByWorkerName(workerName) {
|
|
333
|
-
const db = getDb();
|
|
334
|
-
const row = db
|
|
335
|
-
.prepare(`SELECT team_id FROM team_members WHERE worker_name = ? AND status IN ('pending', 'running') LIMIT 1`)
|
|
336
|
-
.get(workerName);
|
|
337
|
-
return row?.team_id;
|
|
338
|
-
}
|
|
339
|
-
export function cleanupTeam(teamId) {
|
|
340
|
-
const db = getDb();
|
|
341
|
-
db.prepare(`DELETE FROM team_members WHERE team_id = ?`).run(teamId);
|
|
342
|
-
db.prepare(`DELETE FROM agent_teams WHERE id = ?`).run(teamId);
|
|
343
|
-
}
|
|
344
141
|
export function closeDb() {
|
|
345
142
|
if (db) {
|
|
346
143
|
stmtCache = undefined;
|
|
@@ -348,4 +145,8 @@ export function closeDb() {
|
|
|
348
145
|
db = undefined;
|
|
349
146
|
}
|
|
350
147
|
}
|
|
148
|
+
// Re-export for backward compatibility
|
|
149
|
+
export { logConversation, getConversationContext, setConversationTelegramMsgId, getConversationByTelegramMsgId, getRecentConversation, } from "./conversation.js";
|
|
150
|
+
export { addMemory, searchMemories, removeMemory, getMemorySummary } from "./memory.js";
|
|
151
|
+
export { createTeam, addTeamMember, updateTeamMemberResult, getTeam, getTeamMembers, completeTeam, getActiveTeams, getTeamByWorkerName, cleanupTeam, } from "./team-store.js";
|
|
351
152
|
//# sourceMappingURL=db.js.map
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { getDb } from "./db.js";
|
|
2
|
+
// Lazy per-connection prepared statement cache
|
|
3
|
+
let cachedDb;
|
|
4
|
+
let stmtCache;
|
|
5
|
+
function ensureStmtCache() {
|
|
6
|
+
const db = getDb();
|
|
7
|
+
if (db !== cachedDb) {
|
|
8
|
+
cachedDb = db;
|
|
9
|
+
stmtCache = {
|
|
10
|
+
addMemory: db.prepare(`INSERT INTO memories (category, content, source) VALUES (?, ?, ?)`),
|
|
11
|
+
removeMemory: db.prepare(`DELETE FROM memories WHERE id = ?`),
|
|
12
|
+
memorySummary: db.prepare(`SELECT id, category, content FROM memories ORDER BY category, last_accessed DESC`),
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
return stmtCache;
|
|
16
|
+
}
|
|
17
|
+
/** Add a memory to long-term storage. */
|
|
18
|
+
export function addMemory(category, content, source = "user") {
|
|
19
|
+
const cache = ensureStmtCache();
|
|
20
|
+
const result = cache.addMemory.run(category, content, source);
|
|
21
|
+
return result.lastInsertRowid;
|
|
22
|
+
}
|
|
23
|
+
/** Search memories by keyword and/or category. Uses FTS5 when available, falls back to LIKE. */
|
|
24
|
+
export function searchMemories(keyword, category, limit = 20) {
|
|
25
|
+
const db = getDb();
|
|
26
|
+
// Try FTS5 first for keyword search (much faster than LIKE)
|
|
27
|
+
if (keyword) {
|
|
28
|
+
try {
|
|
29
|
+
const catFilter = category ? `AND m.category = ?` : "";
|
|
30
|
+
const params = [keyword + "*"];
|
|
31
|
+
if (category)
|
|
32
|
+
params.push(category);
|
|
33
|
+
params.push(limit);
|
|
34
|
+
const rows = db
|
|
35
|
+
.prepare(`SELECT m.id, m.category, m.content, m.source, m.created_at
|
|
36
|
+
FROM memories_fts f
|
|
37
|
+
JOIN memories m ON f.rowid = m.id
|
|
38
|
+
WHERE memories_fts MATCH ? ${catFilter}
|
|
39
|
+
ORDER BY rank LIMIT ?`)
|
|
40
|
+
.all(...params);
|
|
41
|
+
return rows;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// FTS5 not available — fall through to LIKE
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
// Fallback: LIKE-based search
|
|
48
|
+
const conditions = [];
|
|
49
|
+
const params = [];
|
|
50
|
+
if (keyword) {
|
|
51
|
+
conditions.push(`content LIKE ?`);
|
|
52
|
+
params.push(`%${keyword}%`);
|
|
53
|
+
}
|
|
54
|
+
if (category) {
|
|
55
|
+
conditions.push(`category = ?`);
|
|
56
|
+
params.push(category);
|
|
57
|
+
}
|
|
58
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
59
|
+
params.push(limit);
|
|
60
|
+
const rows = db
|
|
61
|
+
.prepare(`SELECT id, category, content, source, created_at FROM memories ${where} ORDER BY last_accessed DESC LIMIT ?`)
|
|
62
|
+
.all(...params);
|
|
63
|
+
return rows;
|
|
64
|
+
}
|
|
65
|
+
/** Remove a memory by ID. */
|
|
66
|
+
export function removeMemory(id) {
|
|
67
|
+
const cache = ensureStmtCache();
|
|
68
|
+
const result = cache.removeMemory.run(id);
|
|
69
|
+
return result.changes > 0;
|
|
70
|
+
}
|
|
71
|
+
/** Get a compact summary of all memories for injection into system message. */
|
|
72
|
+
export function getMemorySummary() {
|
|
73
|
+
const cache = ensureStmtCache();
|
|
74
|
+
const rows = cache.memorySummary.all();
|
|
75
|
+
if (rows.length === 0)
|
|
76
|
+
return "";
|
|
77
|
+
// Group by category
|
|
78
|
+
const grouped = {};
|
|
79
|
+
for (const r of rows) {
|
|
80
|
+
if (!grouped[r.category])
|
|
81
|
+
grouped[r.category] = [];
|
|
82
|
+
grouped[r.category].push({ id: r.id, content: r.content });
|
|
83
|
+
}
|
|
84
|
+
const sections = Object.entries(grouped).map(([cat, items]) => {
|
|
85
|
+
const lines = items.map((i) => ` - [#${i.id}] ${i.content}`).join("\n");
|
|
86
|
+
return `**${cat}**:\n${lines}`;
|
|
87
|
+
});
|
|
88
|
+
return sections.join("\n");
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=memory.js.map
|