@iletai/nzb 1.6.4 → 1.7.3

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.
@@ -5,8 +5,8 @@ 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 { addMemory, getDb, removeMemory, searchMemories } from "../store/db.js";
9
- import { getCurrentSourceChannel } from "./orchestrator.js";
8
+ import { getDb } from "../store/db.js";
9
+ import { addMemory, removeMemory, searchMemories } from "../store/memory.js";
10
10
  import { createSkill, listSkills, removeSkill } from "./skills.js";
11
11
  function isTimeoutError(err) {
12
12
  const msg = err instanceof Error ? err.message : String(err);
@@ -79,7 +79,7 @@ export function createTools(deps) {
79
79
  session,
80
80
  workingDir: args.working_dir,
81
81
  status: "idle",
82
- originChannel: getCurrentSourceChannel(),
82
+ originChannel: deps.getCurrentSourceChannel(),
83
83
  };
84
84
  deps.workers.set(args.name, worker);
85
85
  deps.onWorkerEvent?.({ type: "created", name: args.name, workingDir: args.working_dir });
@@ -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
@@ -221,6 +221,34 @@ export function createTools(deps) {
221
221
  return `Worker '${args.name}' terminated.`;
222
222
  },
223
223
  }),
224
+ defineTool("kill_worker", {
225
+ description: "Force-kill a stuck or unresponsive worker session by name. " +
226
+ "Use when a worker is hanging or no longer needed.",
227
+ parameters: z.object({
228
+ name: z.string().describe("Name of the worker to kill"),
229
+ }),
230
+ handler: async (args) => {
231
+ const worker = deps.workers.get(args.name);
232
+ if (!worker)
233
+ return `No worker found with name '${args.name}'.`;
234
+ try {
235
+ worker.session.disconnect().catch(() => { });
236
+ }
237
+ catch {
238
+ // Session may already be destroyed
239
+ }
240
+ deps.workers.delete(args.name);
241
+ try {
242
+ getDb()
243
+ .prepare(`DELETE FROM worker_sessions WHERE name = ?`)
244
+ .run(args.name);
245
+ }
246
+ catch {
247
+ // DB cleanup is best-effort
248
+ }
249
+ return `Worker '${args.name}' force-killed.`;
250
+ },
251
+ }),
224
252
  // ── Agent Team Tools ──────────────────────────────────────────
225
253
  defineTool("create_agent_team", {
226
254
  description: "Create an agent team — multiple workers collaborating on a task in parallel. Each member gets a role " +
@@ -264,7 +292,7 @@ export function createTools(deps) {
264
292
  }
265
293
  }
266
294
  const teamId = args.team_name;
267
- const originChannel = getCurrentSourceChannel();
295
+ const originChannel = deps.getCurrentSourceChannel();
268
296
  const { createTeam: dbCreateTeam, addTeamMember: dbAddTeamMember } = await import("../store/db.js");
269
297
  dbCreateTeam(teamId, args.task_description, originChannel);
270
298
  const teamInfo = {
@@ -319,7 +347,7 @@ export function createTools(deps) {
319
347
  deps.onWorkerComplete(member.name, errMsg);
320
348
  })
321
349
  .finally(() => {
322
- session.destroy().catch(() => { });
350
+ session.disconnect().catch(() => { });
323
351
  deps.workers.delete(member.name);
324
352
  try {
325
353
  getDb().prepare(`DELETE FROM worker_sessions WHERE name = ?`).run(member.name);
@@ -341,10 +369,7 @@ export function createTools(deps) {
341
369
  defineTool("get_team_status", {
342
370
  description: "Get the status of agent teams — shows active teams, their members, and progress.",
343
371
  parameters: z.object({
344
- team_name: z
345
- .string()
346
- .optional()
347
- .describe("Specific team name to check. Omit to list all active teams."),
372
+ team_name: z.string().optional().describe("Specific team name to check. Omit to list all active teams."),
348
373
  }),
349
374
  handler: async (args) => {
350
375
  if (args.team_name) {
@@ -368,9 +393,7 @@ export function createTools(deps) {
368
393
  : worker?.status === "error"
369
394
  ? "❌ error"
370
395
  : "🔄 pending";
371
- const elapsed = worker?.startedAt
372
- ? `${Math.round((Date.now() - worker.startedAt) / 1000)}s`
373
- : "";
396
+ const elapsed = worker?.startedAt ? `${Math.round((Date.now() - worker.startedAt) / 1000)}s` : "";
374
397
  lines.push(` ${status} ${memberName} ${elapsed}`);
375
398
  }
376
399
  return lines.join("\n");
@@ -493,7 +516,7 @@ export function createTools(deps) {
493
516
  session,
494
517
  workingDir: "(attached)",
495
518
  status: "idle",
496
- originChannel: getCurrentSourceChannel(),
519
+ originChannel: deps.getCurrentSourceChannel(),
497
520
  };
498
521
  deps.workers.set(args.name, worker);
499
522
  const db = getDb();
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
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
- /* best effort */
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,18 +198,18 @@ async function shutdown() {
197
198
  try {
198
199
  await stopBot();
199
200
  }
200
- catch {
201
- /* best effort */
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
205
- await Promise.allSettled(Array.from(workers.values()).map((w) => w.session.destroy()));
206
+ await Promise.allSettled(Array.from(workers.values()).map((w) => w.session.disconnect()));
206
207
  workers.clear();
207
208
  try {
208
209
  await stopClient();
209
210
  }
210
- catch {
211
- /* best effort */
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,18 +230,18 @@ export async function restartDaemon() {
229
230
  try {
230
231
  await stopBot();
231
232
  }
232
- catch {
233
- /* best effort */
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
237
- await Promise.allSettled(Array.from(activeWorkers.values()).map((w) => w.session.destroy()));
238
+ await Promise.allSettled(Array.from(activeWorkers.values()).map((w) => w.session.disconnect()));
238
239
  activeWorkers.clear();
239
240
  try {
240
241
  await stopClient();
241
242
  }
242
- catch {
243
- /* best effort */
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
- /* best-effort */
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,8 +1,7 @@
1
1
  import Database from "better-sqlite3";
2
2
  import { DB_PATH, ensureNZBHome } from "../paths.js";
3
3
  let db;
4
- let logInsertCount = 0;
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) {
@@ -115,17 +114,11 @@ export function getDb() {
115
114
  // FTS5 may not be available — will fall back to LIKE
116
115
  console.log("[nzb] FTS5 not available, using LIKE fallback for memory search");
117
116
  }
118
- // Initialize cached prepared statements for hot-path operations
117
+ // Initialize cached prepared statements for state operations
119
118
  stmtCache = {
120
119
  getState: db.prepare(`SELECT value FROM nzb_state WHERE key = ?`),
121
120
  setState: db.prepare(`INSERT OR REPLACE INTO nzb_state (key, value) VALUES (?, ?)`),
122
121
  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
122
  };
130
123
  }
131
124
  return db;
@@ -144,203 +137,6 @@ export function deleteState(key) {
144
137
  getDb(); // ensure init
145
138
  stmtCache.deleteState.run(key);
146
139
  }
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
140
  export function closeDb() {
345
141
  if (db) {
346
142
  stmtCache = undefined;
@@ -348,4 +144,8 @@ export function closeDb() {
348
144
  db = undefined;
349
145
  }
350
146
  }
147
+ // Re-export for backward compatibility
148
+ export { logConversation, getConversationContext, setConversationTelegramMsgId, getConversationByTelegramMsgId, getRecentConversation, } from "./conversation.js";
149
+ export { addMemory, searchMemories, removeMemory, getMemorySummary } from "./memory.js";
150
+ export { createTeam, addTeamMember, updateTeamMemberResult, getTeam, getTeamMembers, completeTeam, getActiveTeams, getTeamByWorkerName, cleanupTeam, } from "./team-store.js";
351
151
  //# 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