@kevin5251984/guild 0.2.12

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.
Files changed (70) hide show
  1. package/LICENSE +21 -0
  2. package/bin/guildd.mjs +20 -0
  3. package/cordis.yml +24 -0
  4. package/package.json +52 -0
  5. package/src/agent-file.ts +125 -0
  6. package/src/browser.ts +668 -0
  7. package/src/catalog/default-bots.ts +263 -0
  8. package/src/catalog/skills.ts +128 -0
  9. package/src/catalog/subagents.ts +70 -0
  10. package/src/chat-parts.ts +71 -0
  11. package/src/cli-args.ts +75 -0
  12. package/src/cli.ts +60 -0
  13. package/src/compact.ts +355 -0
  14. package/src/cordis.d.ts +40 -0
  15. package/src/db.ts +653 -0
  16. package/src/generate.ts +673 -0
  17. package/src/handlers.ts +1623 -0
  18. package/src/harness.ts +326 -0
  19. package/src/host-agents.ts +137 -0
  20. package/src/host-browse.ts +199 -0
  21. package/src/host-skills.ts +150 -0
  22. package/src/image-gen.ts +270 -0
  23. package/src/index.ts +12 -0
  24. package/src/llm.ts +993 -0
  25. package/src/mcp.ts +563 -0
  26. package/src/memory.ts +159 -0
  27. package/src/mention.ts +176 -0
  28. package/src/oauth.ts +1474 -0
  29. package/src/plugins/api.ts +8 -0
  30. package/src/plugins/chat.ts +31 -0
  31. package/src/plugins/harness.ts +77 -0
  32. package/src/plugins/llm.ts +50 -0
  33. package/src/plugins/mcp.ts +58 -0
  34. package/src/plugins/memory.ts +42 -0
  35. package/src/plugins/oauth.ts +47 -0
  36. package/src/plugins/server.ts +126 -0
  37. package/src/plugins/store.ts +29 -0
  38. package/src/plugins/tools.ts +79 -0
  39. package/src/public/buddy.js +432 -0
  40. package/src/public/chat.css +3045 -0
  41. package/src/public/chat.html +5834 -0
  42. package/src/public/favicon-16.png +0 -0
  43. package/src/public/favicon-16.svg +10 -0
  44. package/src/public/favicon-32.png +0 -0
  45. package/src/public/favicon.ico +0 -0
  46. package/src/public/favicon.svg +13 -0
  47. package/src/public/i18n.js +663 -0
  48. package/src/public/index.html +143 -0
  49. package/src/public/library.html +678 -0
  50. package/src/public/mcp-add.html +126 -0
  51. package/src/public/md.js +332 -0
  52. package/src/public/rpg/inn-street.jpg +0 -0
  53. package/src/public/settings.html +795 -0
  54. package/src/public/skills-add.html +212 -0
  55. package/src/public/studio.html +1181 -0
  56. package/src/public/style.css +1678 -0
  57. package/src/public/subagents-add.html +152 -0
  58. package/src/router.ts +978 -0
  59. package/src/send-budget.ts +52 -0
  60. package/src/server.ts +1 -0
  61. package/src/skill-import.ts +250 -0
  62. package/src/slash.ts +15 -0
  63. package/src/start.ts +103 -0
  64. package/src/store.ts +1208 -0
  65. package/src/subagent.ts +355 -0
  66. package/src/tools.ts +818 -0
  67. package/src/trajectory.ts +339 -0
  68. package/src/usage.ts +111 -0
  69. package/vendor/protocol/package.json +19 -0
  70. package/vendor/protocol/src/index.ts +159 -0
package/src/db.ts ADDED
@@ -0,0 +1,653 @@
1
+ import {
2
+ existsSync,
3
+ mkdirSync,
4
+ readdirSync,
5
+ readFileSync,
6
+ rmSync,
7
+ } from "node:fs";
8
+ import { dirname, join, resolve } from "node:path";
9
+ import { DatabaseSync } from "node:sqlite";
10
+ import type {
11
+ ChatAttachment,
12
+ ChatMessage,
13
+ ChatPart,
14
+ ChatUsage,
15
+ Room,
16
+ } from "@guild/protocol";
17
+ import type { TrajectoryDraft, TrajectoryEvent } from "./trajectory.ts";
18
+
19
+ export const GUILD_DB_FILE = "guild.sqlite";
20
+ const SCHEMA_VERSION = "1";
21
+
22
+ const SCHEMA = `
23
+ PRAGMA journal_mode = WAL;
24
+ PRAGMA foreign_keys = ON;
25
+ PRAGMA busy_timeout = 5000;
26
+
27
+ CREATE TABLE IF NOT EXISTS meta (
28
+ key TEXT PRIMARY KEY,
29
+ value TEXT NOT NULL
30
+ );
31
+
32
+ CREATE TABLE IF NOT EXISTS rooms (
33
+ id TEXT PRIMARY KEY,
34
+ kind TEXT NOT NULL CHECK (kind IN ('channel', 'dm')),
35
+ name TEXT NOT NULL,
36
+ member_ids TEXT NOT NULL DEFAULT '[]',
37
+ created_at TEXT NOT NULL
38
+ );
39
+
40
+ CREATE TABLE IF NOT EXISTS messages (
41
+ id TEXT PRIMARY KEY,
42
+ room_id TEXT NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
43
+ seq INTEGER NOT NULL,
44
+ author TEXT NOT NULL,
45
+ body TEXT NOT NULL,
46
+ parts TEXT,
47
+ reply_to TEXT,
48
+ attachments TEXT,
49
+ usage TEXT,
50
+ created_at TEXT NOT NULL,
51
+ finished_at TEXT,
52
+ steer INTEGER NOT NULL DEFAULT 0,
53
+ steer_bot_id TEXT,
54
+ UNIQUE (room_id, seq)
55
+ );
56
+
57
+ CREATE INDEX IF NOT EXISTS messages_room_seq ON messages(room_id, seq);
58
+
59
+ CREATE TABLE IF NOT EXISTS trajectory (
60
+ room_id TEXT NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
61
+ seq INTEGER NOT NULL,
62
+ ts TEXT NOT NULL,
63
+ turn_id TEXT NOT NULL,
64
+ bot_id TEXT,
65
+ kind TEXT NOT NULL,
66
+ summary TEXT NOT NULL,
67
+ payload TEXT,
68
+ result TEXT,
69
+ duration_ms INTEGER,
70
+ is_error INTEGER NOT NULL DEFAULT 0,
71
+ PRIMARY KEY (room_id, seq)
72
+ );
73
+
74
+ CREATE TABLE IF NOT EXISTS compact (
75
+ room_id TEXT PRIMARY KEY REFERENCES rooms(id) ON DELETE CASCADE,
76
+ through_id TEXT NOT NULL,
77
+ summary TEXT NOT NULL,
78
+ updated_at TEXT NOT NULL,
79
+ message_count INTEGER NOT NULL DEFAULT 0
80
+ );
81
+ `;
82
+
83
+ type CompactRow = {
84
+ throughId: string;
85
+ summary: string;
86
+ updatedAt: string;
87
+ messageCount: number;
88
+ };
89
+
90
+ function asString(value: unknown, fallback = ""): string {
91
+ return typeof value === "string" ? value : fallback;
92
+ }
93
+
94
+ function asNumber(value: unknown, fallback = 0): number {
95
+ return typeof value === "number" && Number.isFinite(value) ? value : fallback;
96
+ }
97
+
98
+ function parseJson<T>(raw: unknown, fallback: T): T {
99
+ if (typeof raw !== "string" || !raw) return fallback;
100
+ try {
101
+ return JSON.parse(raw) as T;
102
+ } catch {
103
+ return fallback;
104
+ }
105
+ }
106
+
107
+ function isChatMessage(value: unknown): value is ChatMessage {
108
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
109
+ const rec = value as Record<string, unknown>;
110
+ return (
111
+ typeof rec.id === "string" &&
112
+ typeof rec.roomId === "string" &&
113
+ typeof rec.author === "string" &&
114
+ typeof rec.body === "string" &&
115
+ typeof rec.createdAt === "string"
116
+ );
117
+ }
118
+
119
+ function parseJsonlMessages(raw: string): ChatMessage[] {
120
+ const out: ChatMessage[] = [];
121
+ for (const line of raw.split("\n")) {
122
+ const trimmed = line.trim();
123
+ if (!trimmed) continue;
124
+ try {
125
+ const parsed: unknown = JSON.parse(trimmed);
126
+ if (isChatMessage(parsed)) out.push(parsed);
127
+ } catch {
128
+ /* skip */
129
+ }
130
+ }
131
+ return out;
132
+ }
133
+
134
+ function parseJsonlTrajectory(raw: string): TrajectoryEvent[] {
135
+ const out: TrajectoryEvent[] = [];
136
+ for (const line of raw.split("\n")) {
137
+ const trimmed = line.trim();
138
+ if (!trimmed) continue;
139
+ try {
140
+ const parsed = JSON.parse(trimmed) as TrajectoryEvent;
141
+ if (parsed && typeof parsed.kind === "string") out.push(parsed);
142
+ } catch {
143
+ /* skip */
144
+ }
145
+ }
146
+ return out;
147
+ }
148
+
149
+ function messageFromRow(row: Record<string, unknown>): ChatMessage {
150
+ const parts = parseJson<ChatPart[] | null>(row.parts, null);
151
+ const attachments = parseJson<ChatAttachment[] | null>(row.attachments, null);
152
+ const usage = parseJson<ChatUsage | null>(row.usage, null);
153
+ const message: ChatMessage = {
154
+ id: asString(row.id),
155
+ roomId: asString(row.room_id),
156
+ author: asString(row.author),
157
+ body: asString(row.body),
158
+ createdAt: asString(row.created_at),
159
+ };
160
+ if (parts?.length) message.parts = parts;
161
+ const replyTo = asString(row.reply_to);
162
+ if (replyTo) message.replyTo = replyTo;
163
+ if (attachments?.length) message.attachments = attachments;
164
+ if (usage) message.usage = usage;
165
+ const finishedAt = asString(row.finished_at);
166
+ if (finishedAt) message.finishedAt = finishedAt;
167
+ if (asNumber(row.steer) === 1) message.steer = true;
168
+ const steerBotId = asString(row.steer_bot_id);
169
+ if (steerBotId) message.steerBotId = steerBotId;
170
+ return message;
171
+ }
172
+
173
+ function roomFromRow(row: Record<string, unknown>): Room {
174
+ const memberIds = parseJson<string[]>(row.member_ids, []);
175
+ return {
176
+ id: asString(row.id),
177
+ kind: asString(row.kind) === "dm" ? "dm" : "channel",
178
+ name: asString(row.name),
179
+ memberIds: Array.isArray(memberIds)
180
+ ? memberIds.filter((id) => typeof id === "string")
181
+ : [],
182
+ createdAt: asString(row.created_at),
183
+ };
184
+ }
185
+
186
+ function trajectoryFromRow(row: Record<string, unknown>): TrajectoryEvent {
187
+ const event: TrajectoryEvent = {
188
+ seq: asNumber(row.seq),
189
+ ts: asString(row.ts),
190
+ turnId: asString(row.turn_id),
191
+ kind: asString(row.kind) as TrajectoryEvent["kind"],
192
+ summary: asString(row.summary),
193
+ };
194
+ const botId = asString(row.bot_id);
195
+ if (botId) event.botId = botId;
196
+ if (row.payload != null && row.payload !== "") {
197
+ event.payload = parseJson<unknown>(row.payload, undefined);
198
+ }
199
+ const result = asString(row.result);
200
+ if (result) event.result = result;
201
+ if (row.duration_ms != null) event.durationMs = asNumber(row.duration_ms);
202
+ if (asNumber(row.is_error) === 1) event.isError = true;
203
+ return event;
204
+ }
205
+
206
+ export class GuildDb {
207
+ readonly sqlite: DatabaseSync;
208
+
209
+ constructor(readonly path: string) {
210
+ mkdirSync(dirname(path), { recursive: true });
211
+ this.sqlite = new DatabaseSync(path, { timeout: 5000 });
212
+ this.sqlite.exec(SCHEMA);
213
+ try {
214
+ this.sqlite.exec("ALTER TABLE messages ADD COLUMN steer_bot_id TEXT");
215
+ } catch {
216
+ /* column already exists on fresh schema */
217
+ }
218
+ this.sqlite.prepare(
219
+ "INSERT OR IGNORE INTO meta (key, value) VALUES ('schema', ?)",
220
+ ).run(SCHEMA_VERSION);
221
+ }
222
+
223
+ close(): void {
224
+ this.sqlite.close();
225
+ }
226
+
227
+ importLegacyFiles(dataDir: string): void {
228
+ const root = join(dataDir, "rooms");
229
+ if (!existsSync(root)) return;
230
+ const ids = readdirSync(root, { withFileTypes: true })
231
+ .filter((entry) => entry.isDirectory())
232
+ .map((entry) => entry.name);
233
+ for (const id of ids) this.importRoomDir(join(root, id), id);
234
+ }
235
+
236
+ upsertRoom(room: Room): void {
237
+ this.sqlite
238
+ .prepare(
239
+ `INSERT INTO rooms (id, kind, name, member_ids, created_at)
240
+ VALUES (?, ?, ?, ?, ?)
241
+ ON CONFLICT(id) DO UPDATE SET
242
+ kind = excluded.kind,
243
+ name = excluded.name,
244
+ member_ids = excluded.member_ids,
245
+ created_at = excluded.created_at`,
246
+ )
247
+ .run(
248
+ room.id,
249
+ room.kind,
250
+ room.name,
251
+ JSON.stringify(room.memberIds),
252
+ room.createdAt,
253
+ );
254
+ }
255
+
256
+ getRoom(id: string): Room | null {
257
+ const row = this.sqlite
258
+ .prepare("SELECT * FROM rooms WHERE id = ?")
259
+ .get(id) as Record<string, unknown> | undefined;
260
+ return row ? roomFromRow(row) : null;
261
+ }
262
+
263
+ listRooms(): Room[] {
264
+ const rows = this.sqlite
265
+ .prepare("SELECT * FROM rooms ORDER BY created_at ASC, id ASC")
266
+ .all() as Record<string, unknown>[];
267
+ return rows.map(roomFromRow);
268
+ }
269
+
270
+ deleteRoom(id: string): void {
271
+ this.sqlite.prepare("DELETE FROM rooms WHERE id = ?").run(id);
272
+ }
273
+
274
+ listMessages(roomId: string): ChatMessage[] {
275
+ const rows = this.sqlite
276
+ .prepare("SELECT * FROM messages WHERE room_id = ? ORDER BY seq ASC")
277
+ .all(roomId) as Record<string, unknown>[];
278
+ return rows.map(messageFromRow);
279
+ }
280
+
281
+ peekLastMessage(roomId: string): ChatMessage | undefined {
282
+ const row = this.sqlite
283
+ .prepare(
284
+ "SELECT * FROM messages WHERE room_id = ? ORDER BY seq DESC LIMIT 1",
285
+ )
286
+ .get(roomId) as Record<string, unknown> | undefined;
287
+ return row ? messageFromRow(row) : undefined;
288
+ }
289
+
290
+ appendMessage(message: ChatMessage): void {
291
+ const seq = this.nextMessageSeq(message.roomId);
292
+ this.insertMessage(message, seq);
293
+ }
294
+
295
+ replaceMessages(roomId: string, messages: ChatMessage[]): void {
296
+ this.sqlite.exec("BEGIN");
297
+ try {
298
+ this.sqlite.prepare("DELETE FROM messages WHERE room_id = ?").run(roomId);
299
+ messages.forEach((message, index) => this.insertMessage(message, index));
300
+ this.sqlite.exec("COMMIT");
301
+ } catch (error) {
302
+ this.sqlite.exec("ROLLBACK");
303
+ throw error;
304
+ }
305
+ }
306
+
307
+ updateMessageBody(roomId: string, messageId: string, body: string): ChatMessage | null {
308
+ const result = this.sqlite
309
+ .prepare("UPDATE messages SET body = ? WHERE room_id = ? AND id = ?")
310
+ .run(body, roomId, messageId);
311
+ if (!result.changes) return null;
312
+ return this.getMessage(roomId, messageId);
313
+ }
314
+
315
+ replaceMessage(
316
+ roomId: string,
317
+ messageId: string,
318
+ patch: {
319
+ body: string;
320
+ parts?: ChatPart[];
321
+ usage?: ChatUsage;
322
+ createdAt: string;
323
+ finishedAt: string;
324
+ },
325
+ ): ChatMessage | null {
326
+ const result = this.sqlite
327
+ .prepare(
328
+ `UPDATE messages
329
+ SET body = ?, parts = ?, usage = ?, created_at = ?, finished_at = ?
330
+ WHERE room_id = ? AND id = ?`,
331
+ )
332
+ .run(
333
+ patch.body,
334
+ patch.parts?.length ? JSON.stringify(patch.parts) : null,
335
+ patch.usage ? JSON.stringify(patch.usage) : null,
336
+ patch.createdAt,
337
+ patch.finishedAt,
338
+ roomId,
339
+ messageId,
340
+ );
341
+ if (!result.changes) return null;
342
+ return this.getMessage(roomId, messageId);
343
+ }
344
+
345
+ deleteMessage(roomId: string, messageId: string): ChatMessage | null {
346
+ const existing = this.getMessage(roomId, messageId);
347
+ if (!existing) return null;
348
+ this.sqlite
349
+ .prepare("DELETE FROM messages WHERE room_id = ? AND id = ?")
350
+ .run(roomId, messageId);
351
+ this.sqlite
352
+ .prepare("DELETE FROM trajectory WHERE room_id = ? AND turn_id = ?")
353
+ .run(roomId, messageId);
354
+ return existing;
355
+ }
356
+
357
+ truncateAfter(roomId: string, messageId: string): ChatMessage[] | null {
358
+ const row = this.sqlite
359
+ .prepare("SELECT seq FROM messages WHERE room_id = ? AND id = ?")
360
+ .get(roomId, messageId) as { seq?: number } | undefined;
361
+ if (!row || typeof row.seq !== "number") return null;
362
+ this.sqlite
363
+ .prepare("DELETE FROM messages WHERE room_id = ? AND seq > ?")
364
+ .run(roomId, row.seq);
365
+ return this.listMessages(roomId);
366
+ }
367
+
368
+ listTrajectory(roomId: string): TrajectoryEvent[] {
369
+ const rows = this.sqlite
370
+ .prepare("SELECT * FROM trajectory WHERE room_id = ? ORDER BY seq ASC")
371
+ .all(roomId) as Record<string, unknown>[];
372
+ return rows.map(trajectoryFromRow);
373
+ }
374
+
375
+ appendTrajectory(roomId: string, drafts: TrajectoryDraft[]): TrajectoryEvent[] {
376
+ let seq = this.lastTrajectorySeq(roomId);
377
+ const written: TrajectoryEvent[] = [];
378
+ this.sqlite.exec("BEGIN");
379
+ try {
380
+ const insert = this.sqlite.prepare(
381
+ `INSERT INTO trajectory (
382
+ room_id, seq, ts, turn_id, bot_id, kind, summary, payload, result, duration_ms, is_error
383
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
384
+ );
385
+ for (const draft of drafts) {
386
+ seq += 1;
387
+ const event: TrajectoryEvent = { ...draft, seq };
388
+ insert.run(
389
+ roomId,
390
+ event.seq,
391
+ event.ts,
392
+ event.turnId,
393
+ event.botId ?? null,
394
+ event.kind,
395
+ event.summary,
396
+ event.payload === undefined ? null : JSON.stringify(event.payload),
397
+ event.result ?? null,
398
+ event.durationMs ?? null,
399
+ event.isError ? 1 : 0,
400
+ );
401
+ written.push(event);
402
+ }
403
+ this.sqlite.exec("COMMIT");
404
+ } catch (error) {
405
+ this.sqlite.exec("ROLLBACK");
406
+ throw error;
407
+ }
408
+ return written;
409
+ }
410
+
411
+ readCompact(roomId: string): CompactRow | null {
412
+ const row = this.sqlite
413
+ .prepare("SELECT * FROM compact WHERE room_id = ?")
414
+ .get(roomId) as Record<string, unknown> | undefined;
415
+ if (!row) return null;
416
+ const throughId = asString(row.through_id);
417
+ const summary = asString(row.summary);
418
+ if (!throughId || !summary) return null;
419
+ return {
420
+ throughId,
421
+ summary,
422
+ updatedAt: asString(row.updated_at),
423
+ messageCount: asNumber(row.message_count),
424
+ };
425
+ }
426
+
427
+ writeCompact(roomId: string, compact: CompactRow): void {
428
+ this.sqlite
429
+ .prepare(
430
+ `INSERT INTO compact (room_id, through_id, summary, updated_at, message_count)
431
+ VALUES (?, ?, ?, ?, ?)
432
+ ON CONFLICT(room_id) DO UPDATE SET
433
+ through_id = excluded.through_id,
434
+ summary = excluded.summary,
435
+ updated_at = excluded.updated_at,
436
+ message_count = excluded.message_count`,
437
+ )
438
+ .run(
439
+ roomId,
440
+ compact.throughId,
441
+ compact.summary,
442
+ compact.updatedAt,
443
+ compact.messageCount,
444
+ );
445
+ }
446
+
447
+ private getMessage(roomId: string, messageId: string): ChatMessage | null {
448
+ const row = this.sqlite
449
+ .prepare("SELECT * FROM messages WHERE room_id = ? AND id = ?")
450
+ .get(roomId, messageId) as Record<string, unknown> | undefined;
451
+ return row ? messageFromRow(row) : null;
452
+ }
453
+
454
+ private nextMessageSeq(roomId: string): number {
455
+ const row = this.sqlite
456
+ .prepare("SELECT COALESCE(MAX(seq), -1) AS seq FROM messages WHERE room_id = ?")
457
+ .get(roomId) as { seq?: number } | undefined;
458
+ return asNumber(row?.seq, -1) + 1;
459
+ }
460
+
461
+ private lastTrajectorySeq(roomId: string): number {
462
+ const row = this.sqlite
463
+ .prepare("SELECT COALESCE(MAX(seq), -1) AS seq FROM trajectory WHERE room_id = ?")
464
+ .get(roomId) as { seq?: number } | undefined;
465
+ return asNumber(row?.seq, -1);
466
+ }
467
+
468
+ private insertMessage(message: ChatMessage, seq: number): void {
469
+ this.sqlite
470
+ .prepare(
471
+ `INSERT INTO messages (
472
+ id, room_id, seq, author, body, parts, reply_to, attachments, usage,
473
+ created_at, finished_at, steer, steer_bot_id
474
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
475
+ )
476
+ .run(
477
+ message.id,
478
+ message.roomId,
479
+ seq,
480
+ message.author,
481
+ message.body,
482
+ message.parts?.length ? JSON.stringify(message.parts) : null,
483
+ message.replyTo ?? null,
484
+ message.attachments?.length ? JSON.stringify(message.attachments) : null,
485
+ message.usage ? JSON.stringify(message.usage) : null,
486
+ message.createdAt,
487
+ message.finishedAt ?? null,
488
+ message.steer ? 1 : 0,
489
+ message.steerBotId ?? null,
490
+ );
491
+ }
492
+
493
+ private importRoomDir(dir: string, id: string): void {
494
+ const roomFile = join(dir, "room.json");
495
+ if (existsSync(roomFile) && !this.getRoom(id)) {
496
+ try {
497
+ const parsed = JSON.parse(readFileSync(roomFile, "utf8")) as Room;
498
+ if (parsed && parsed.id === id && (parsed.kind === "channel" || parsed.kind === "dm")) {
499
+ this.upsertRoom({
500
+ id,
501
+ kind: parsed.kind,
502
+ name: typeof parsed.name === "string" ? parsed.name : id,
503
+ memberIds: Array.isArray(parsed.memberIds)
504
+ ? parsed.memberIds.filter((item): item is string => typeof item === "string")
505
+ : [],
506
+ createdAt:
507
+ typeof parsed.createdAt === "string"
508
+ ? parsed.createdAt
509
+ : "2026-01-01T00:00:00.000Z",
510
+ });
511
+ }
512
+ } catch {
513
+ /* skip bad room.json */
514
+ }
515
+ }
516
+ if (!this.getRoom(id)) return;
517
+
518
+ if (this.messageCount(id) === 0) {
519
+ const jsonl = join(dir, "messages.jsonl");
520
+ const json = join(dir, "messages.json");
521
+ let messages: ChatMessage[] = [];
522
+ if (existsSync(jsonl)) {
523
+ messages = parseJsonlMessages(readFileSync(jsonl, "utf8"));
524
+ } else if (existsSync(json)) {
525
+ try {
526
+ const parsed = JSON.parse(readFileSync(json, "utf8")) as unknown;
527
+ messages = Array.isArray(parsed) ? parsed.filter(isChatMessage) : [];
528
+ } catch {
529
+ messages = [];
530
+ }
531
+ }
532
+ if (messages.length) this.replaceMessages(id, messages);
533
+ }
534
+
535
+ if (this.trajectoryCount(id) === 0) {
536
+ const path = join(dir, "trajectory.jsonl");
537
+ if (existsSync(path)) {
538
+ const events = parseJsonlTrajectory(readFileSync(path, "utf8"));
539
+ if (events.length) {
540
+ this.sqlite.exec("BEGIN");
541
+ try {
542
+ const insert = this.sqlite.prepare(
543
+ `INSERT INTO trajectory (
544
+ room_id, seq, ts, turn_id, bot_id, kind, summary, payload, result, duration_ms, is_error
545
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
546
+ );
547
+ for (const event of events) {
548
+ insert.run(
549
+ id,
550
+ event.seq,
551
+ event.ts,
552
+ event.turnId,
553
+ event.botId ?? null,
554
+ event.kind,
555
+ event.summary,
556
+ event.payload === undefined ? null : JSON.stringify(event.payload),
557
+ event.result ?? null,
558
+ event.durationMs ?? null,
559
+ event.isError ? 1 : 0,
560
+ );
561
+ }
562
+ this.sqlite.exec("COMMIT");
563
+ } catch (error) {
564
+ this.sqlite.exec("ROLLBACK");
565
+ throw error;
566
+ }
567
+ }
568
+ }
569
+ }
570
+
571
+ if (!this.readCompact(id)) {
572
+ const path = join(dir, "compact.json");
573
+ if (existsSync(path)) {
574
+ try {
575
+ const parsed = JSON.parse(readFileSync(path, "utf8")) as {
576
+ throughId?: string;
577
+ summary?: string;
578
+ updatedAt?: string;
579
+ messageCount?: number;
580
+ };
581
+ if (parsed.throughId && parsed.summary) {
582
+ this.writeCompact(id, {
583
+ throughId: parsed.throughId,
584
+ summary: parsed.summary,
585
+ updatedAt: parsed.updatedAt || "",
586
+ messageCount: Number(parsed.messageCount) || 0,
587
+ });
588
+ }
589
+ } catch {
590
+ /* skip */
591
+ }
592
+ }
593
+ }
594
+
595
+ if (this.getRoom(id)) rmIfExists(join(dir, "room.json"));
596
+ if (this.messageCount(id) > 0) {
597
+ rmIfExists(join(dir, "messages.json"));
598
+ rmIfExists(join(dir, "messages.jsonl"));
599
+ }
600
+ if (this.trajectoryCount(id) > 0) {
601
+ rmIfExists(join(dir, "trajectory.jsonl"));
602
+ }
603
+ if (this.readCompact(id)) rmIfExists(join(dir, "compact.json"));
604
+ }
605
+
606
+ private messageCount(roomId: string): number {
607
+ const row = this.sqlite
608
+ .prepare("SELECT COUNT(*) AS n FROM messages WHERE room_id = ?")
609
+ .get(roomId) as { n?: number } | undefined;
610
+ return asNumber(row?.n);
611
+ }
612
+
613
+ private trajectoryCount(roomId: string): number {
614
+ const row = this.sqlite
615
+ .prepare("SELECT COUNT(*) AS n FROM trajectory WHERE room_id = ?")
616
+ .get(roomId) as { n?: number } | undefined;
617
+ return asNumber(row?.n);
618
+ }
619
+ }
620
+
621
+ const OPEN = new Map<string, { db: GuildDb; refs: number }>();
622
+
623
+ export function openGuildDb(dataDir: string): GuildDb {
624
+ const path = resolve(join(dataDir, GUILD_DB_FILE));
625
+ const hit = OPEN.get(path);
626
+ if (hit) {
627
+ hit.refs += 1;
628
+ return hit.db;
629
+ }
630
+ const db = new GuildDb(path);
631
+ OPEN.set(path, { db, refs: 1 });
632
+ return db;
633
+ }
634
+
635
+ export function closeGuildDb(db: GuildDb): void {
636
+ const hit = OPEN.get(db.path);
637
+ if (!hit || hit.db !== db) {
638
+ try {
639
+ db.sqlite.close();
640
+ } catch {
641
+ /* already closed */
642
+ }
643
+ return;
644
+ }
645
+ hit.refs -= 1;
646
+ if (hit.refs > 0) return;
647
+ OPEN.delete(db.path);
648
+ db.sqlite.close();
649
+ }
650
+
651
+ function rmIfExists(path: string): void {
652
+ if (existsSync(path)) rmSync(path, { force: true });
653
+ }