@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.
@@ -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 { addMemory, getDb, removeMemory, searchMemories } from "../store/db.js";
9
- import { getCurrentSourceChannel } from "./orchestrator.js";
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
- // Session may already be gone
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();
@@ -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,8 +198,8 @@ 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
@@ -207,8 +208,8 @@ async function shutdown() {
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,8 +230,8 @@ 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
@@ -239,8 +240,8 @@ export async function restartDaemon() {
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,14 +1,14 @@
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) {
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 hot-path operations
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