opencodekit 0.17.13 → 0.18.0

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 (36) hide show
  1. package/dist/index.js +4 -6
  2. package/dist/template/.opencode/dcp.jsonc +81 -81
  3. package/dist/template/.opencode/memory/memory.db +0 -0
  4. package/dist/template/.opencode/memory.db +0 -0
  5. package/dist/template/.opencode/memory.db-shm +0 -0
  6. package/dist/template/.opencode/memory.db-wal +0 -0
  7. package/dist/template/.opencode/opencode.json +199 -23
  8. package/dist/template/.opencode/opencode.json.tui-migration.bak +1380 -0
  9. package/dist/template/.opencode/package.json +1 -1
  10. package/dist/template/.opencode/plugin/lib/capture.ts +177 -0
  11. package/dist/template/.opencode/plugin/lib/context.ts +194 -0
  12. package/dist/template/.opencode/plugin/lib/curator.ts +234 -0
  13. package/dist/template/.opencode/plugin/lib/db/maintenance.ts +312 -0
  14. package/dist/template/.opencode/plugin/lib/db/observations.ts +299 -0
  15. package/dist/template/.opencode/plugin/lib/db/pipeline.ts +520 -0
  16. package/dist/template/.opencode/plugin/lib/db/schema.ts +356 -0
  17. package/dist/template/.opencode/plugin/lib/db/types.ts +211 -0
  18. package/dist/template/.opencode/plugin/lib/distill.ts +376 -0
  19. package/dist/template/.opencode/plugin/lib/inject.ts +126 -0
  20. package/dist/template/.opencode/plugin/lib/memory-admin-tools.ts +188 -0
  21. package/dist/template/.opencode/plugin/lib/memory-db.ts +54 -936
  22. package/dist/template/.opencode/plugin/lib/memory-helpers.ts +202 -0
  23. package/dist/template/.opencode/plugin/lib/memory-hooks.ts +240 -0
  24. package/dist/template/.opencode/plugin/lib/memory-tools.ts +341 -0
  25. package/dist/template/.opencode/plugin/memory.ts +56 -60
  26. package/dist/template/.opencode/plugin/sessions.ts +372 -93
  27. package/dist/template/.opencode/tui.json +15 -0
  28. package/package.json +1 -1
  29. package/dist/template/.opencode/tool/action-queue.ts +0 -313
  30. package/dist/template/.opencode/tool/memory-admin.ts +0 -445
  31. package/dist/template/.opencode/tool/memory-get.ts +0 -143
  32. package/dist/template/.opencode/tool/memory-read.ts +0 -45
  33. package/dist/template/.opencode/tool/memory-search.ts +0 -264
  34. package/dist/template/.opencode/tool/memory-timeline.ts +0 -105
  35. package/dist/template/.opencode/tool/memory-update.ts +0 -63
  36. package/dist/template/.opencode/tool/observation.ts +0 -357
@@ -0,0 +1,520 @@
1
+ /**
2
+ * Temporal Messages & Distillation Operations
3
+ *
4
+ * Manages the capture pipeline: raw messages → distillations → relevance scoring.
5
+ * Includes TF-IDF-based relevance scoring with BM25, recency decay, and confidence weighting.
6
+ */
7
+
8
+ import { getMemoryDB } from "./schema.js";
9
+ import type {
10
+ DistillationInput,
11
+ DistillationRow,
12
+ DistillationSearchResult,
13
+ TemporalMessageInput,
14
+ TemporalMessageRow,
15
+ } from "./types.js";
16
+ import { MEMORY_CONFIG } from "./types.js";
17
+
18
+ // ============================================================================
19
+ // Temporal Message Operations
20
+ // ============================================================================
21
+
22
+ /**
23
+ * Store a captured message in temporal storage.
24
+ * Uses INSERT OR IGNORE to handle duplicate message_ids gracefully.
25
+ */
26
+ export function storeTemporalMessage(input: TemporalMessageInput): number {
27
+ const db = getMemoryDB();
28
+ const now = new Date().toISOString();
29
+
30
+ // Cap content length
31
+ const content = input.content.slice(
32
+ 0,
33
+ MEMORY_CONFIG.capture.maxContentLength,
34
+ );
35
+
36
+ const result = db
37
+ .query(
38
+ `
39
+ INSERT OR IGNORE INTO temporal_messages
40
+ (session_id, message_id, role, content, token_estimate, time_created, created_at)
41
+ VALUES (?, ?, ?, ?, ?, ?, ?)
42
+ `,
43
+ )
44
+ .run(
45
+ input.session_id,
46
+ input.message_id,
47
+ input.role,
48
+ content,
49
+ input.token_estimate,
50
+ input.time_created,
51
+ now,
52
+ );
53
+
54
+ return Number(result.lastInsertRowid);
55
+ }
56
+
57
+ /**
58
+ * Get undistilled messages for a session (messages not yet part of a distillation).
59
+ */
60
+ export function getUndistilledMessages(
61
+ sessionId: string,
62
+ limit?: number,
63
+ ): TemporalMessageRow[] {
64
+ const db = getMemoryDB();
65
+ const maxLimit = limit ?? MEMORY_CONFIG.distillation.maxMessages;
66
+
67
+ return db
68
+ .query(
69
+ `
70
+ SELECT * FROM temporal_messages
71
+ WHERE session_id = ? AND distillation_id IS NULL
72
+ ORDER BY time_created ASC
73
+ LIMIT ?
74
+ `,
75
+ )
76
+ .all(sessionId, maxLimit) as TemporalMessageRow[];
77
+ }
78
+
79
+ /**
80
+ * Get count of undistilled messages, optionally filtered by session.
81
+ */
82
+ export function getUndistilledMessageCount(sessionId?: string): number {
83
+ const db = getMemoryDB();
84
+
85
+ if (sessionId) {
86
+ const row = db
87
+ .query(
88
+ "SELECT COUNT(*) as count FROM temporal_messages WHERE session_id = ? AND distillation_id IS NULL",
89
+ )
90
+ .get(sessionId) as { count: number };
91
+ return row.count;
92
+ }
93
+
94
+ const row = db
95
+ .query(
96
+ "SELECT COUNT(*) as count FROM temporal_messages WHERE distillation_id IS NULL",
97
+ )
98
+ .get() as { count: number };
99
+ return row.count;
100
+ }
101
+
102
+ /**
103
+ * Mark messages as distilled by linking them to a distillation.
104
+ */
105
+ export function markMessagesDistilled(
106
+ messageIds: number[],
107
+ distillationId: number,
108
+ ): void {
109
+ if (messageIds.length === 0) return;
110
+
111
+ const db = getMemoryDB();
112
+ const placeholders = messageIds.map(() => "?").join(",");
113
+ db.run(
114
+ `UPDATE temporal_messages SET distillation_id = ? WHERE id IN (${placeholders})`,
115
+ [distillationId, ...messageIds],
116
+ );
117
+ }
118
+
119
+ /**
120
+ * Delete old temporal messages beyond retention period.
121
+ */
122
+ export function purgeOldTemporalMessages(olderThanDays?: number): number {
123
+ const db = getMemoryDB();
124
+ const days = olderThanDays ?? MEMORY_CONFIG.capture.maxAgeDays;
125
+ const cutoff = Date.now() - days * 24 * 60 * 60 * 1000;
126
+
127
+ const result = db.run(
128
+ "DELETE FROM temporal_messages WHERE time_created < ? AND distillation_id IS NOT NULL",
129
+ [cutoff],
130
+ );
131
+
132
+ return result.changes;
133
+ }
134
+
135
+ /**
136
+ * Get capture stats for temporal messages.
137
+ */
138
+ export function getCaptureStats(): {
139
+ total: number;
140
+ undistilled: number;
141
+ sessions: number;
142
+ oldestMs: number | null;
143
+ newestMs: number | null;
144
+ } {
145
+ const db = getMemoryDB();
146
+
147
+ const total = (
148
+ db.query("SELECT COUNT(*) as count FROM temporal_messages").get() as {
149
+ count: number;
150
+ }
151
+ ).count;
152
+
153
+ const undistilled = (
154
+ db
155
+ .query(
156
+ "SELECT COUNT(*) as count FROM temporal_messages WHERE distillation_id IS NULL",
157
+ )
158
+ .get() as { count: number }
159
+ ).count;
160
+
161
+ const sessions = (
162
+ db
163
+ .query(
164
+ "SELECT COUNT(DISTINCT session_id) as count FROM temporal_messages",
165
+ )
166
+ .get() as {
167
+ count: number;
168
+ }
169
+ ).count;
170
+
171
+ const timeRange = db
172
+ .query(
173
+ "SELECT MIN(time_created) as oldest, MAX(time_created) as newest FROM temporal_messages",
174
+ )
175
+ .get() as { oldest: number | null; newest: number | null };
176
+
177
+ return {
178
+ total,
179
+ undistilled,
180
+ sessions,
181
+ oldestMs: timeRange.oldest,
182
+ newestMs: timeRange.newest,
183
+ };
184
+ }
185
+
186
+ // ============================================================================
187
+ // Distillation Operations
188
+ // ============================================================================
189
+
190
+ /**
191
+ * Store a new distillation.
192
+ */
193
+ export function storeDistillation(input: DistillationInput): number {
194
+ const db = getMemoryDB();
195
+ const now = Date.now();
196
+ const nowISO = new Date(now).toISOString();
197
+
198
+ const result = db
199
+ .query(
200
+ `
201
+ INSERT INTO distillations
202
+ (session_id, content, terms, message_count, compression_ratio,
203
+ time_start, time_end, time_created, meta_distillation_id, created_at)
204
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
205
+ `,
206
+ )
207
+ .run(
208
+ input.session_id,
209
+ input.content,
210
+ JSON.stringify(input.terms),
211
+ input.message_count,
212
+ input.compression_ratio,
213
+ input.time_start,
214
+ input.time_end,
215
+ now,
216
+ input.meta_distillation_id ?? null,
217
+ nowISO,
218
+ );
219
+
220
+ return Number(result.lastInsertRowid);
221
+ }
222
+
223
+ /**
224
+ * Get distillation by ID.
225
+ */
226
+ export function getDistillationById(id: number): DistillationRow | null {
227
+ const db = getMemoryDB();
228
+ return db
229
+ .query("SELECT * FROM distillations WHERE id = ?")
230
+ .get(id) as DistillationRow | null;
231
+ }
232
+
233
+ /**
234
+ * Get recent distillations, optionally filtered by session.
235
+ */
236
+ export function getRecentDistillations(
237
+ sessionId?: string,
238
+ limit = 10,
239
+ ): DistillationRow[] {
240
+ const db = getMemoryDB();
241
+
242
+ if (sessionId) {
243
+ return db
244
+ .query(
245
+ `SELECT * FROM distillations
246
+ WHERE session_id = ?
247
+ ORDER BY time_created DESC LIMIT ?`,
248
+ )
249
+ .all(sessionId, limit) as DistillationRow[];
250
+ }
251
+
252
+ return db
253
+ .query("SELECT * FROM distillations ORDER BY time_created DESC LIMIT ?")
254
+ .all(limit) as DistillationRow[];
255
+ }
256
+
257
+ /**
258
+ * Search distillations using FTS5.
259
+ */
260
+ export function searchDistillationsFTS(
261
+ query: string,
262
+ limit = 10,
263
+ ): DistillationSearchResult[] {
264
+ const db = getMemoryDB();
265
+
266
+ const ftsQuery = query
267
+ .replace(/['"]/g, '""')
268
+ .split(/\s+/)
269
+ .filter((term) => term.length > 0)
270
+ .map((term) => `"${term}"*`)
271
+ .join(" OR ");
272
+
273
+ if (!ftsQuery) return [];
274
+
275
+ try {
276
+ return db
277
+ .query(
278
+ `
279
+ SELECT d.id, d.session_id,
280
+ substr(d.content, 1, 150) as snippet,
281
+ d.message_count, d.created_at,
282
+ bm25(distillations_fts) as relevance_score
283
+ FROM distillations d
284
+ JOIN distillations_fts fts ON fts.rowid = d.id
285
+ WHERE distillations_fts MATCH ?
286
+ ORDER BY relevance_score
287
+ LIMIT ?
288
+ `,
289
+ )
290
+ .all(ftsQuery, limit) as DistillationSearchResult[];
291
+ } catch {
292
+ // FTS5 failed, fallback to LIKE
293
+ const likePattern = `%${query}%`;
294
+ return db
295
+ .query(
296
+ `
297
+ SELECT id, session_id,
298
+ substr(content, 1, 150) as snippet,
299
+ message_count, created_at,
300
+ 0 as relevance_score
301
+ FROM distillations
302
+ WHERE content LIKE ?
303
+ ORDER BY time_created DESC
304
+ LIMIT ?
305
+ `,
306
+ )
307
+ .all(likePattern, limit) as DistillationSearchResult[];
308
+ }
309
+ }
310
+
311
+ /**
312
+ * Get distillation stats.
313
+ */
314
+ export function getDistillationStats(): {
315
+ total: number;
316
+ sessions: number;
317
+ avgCompression: number;
318
+ totalMessages: number;
319
+ } {
320
+ const db = getMemoryDB();
321
+
322
+ const row = db
323
+ .query(
324
+ `
325
+ SELECT
326
+ COUNT(*) as total,
327
+ COUNT(DISTINCT session_id) as sessions,
328
+ AVG(compression_ratio) as avg_compression,
329
+ SUM(message_count) as total_messages
330
+ FROM distillations
331
+ `,
332
+ )
333
+ .get() as {
334
+ total: number;
335
+ sessions: number;
336
+ avg_compression: number | null;
337
+ total_messages: number | null;
338
+ };
339
+
340
+ return {
341
+ total: row.total,
342
+ sessions: row.sessions,
343
+ avgCompression: row.avg_compression ?? 0,
344
+ totalMessages: row.total_messages ?? 0,
345
+ };
346
+ }
347
+
348
+ // ============================================================================
349
+ // Relevance Scoring & Token Estimation
350
+ // ============================================================================
351
+
352
+ /**
353
+ * Get all observations scored by relevance for injection.
354
+ * Combines BM25 relevance with recency decay and confidence weighting.
355
+ */
356
+ export function getRelevantKnowledge(
357
+ queryTerms: string[],
358
+ options: {
359
+ tokenBudget?: number;
360
+ minScore?: number;
361
+ limit?: number;
362
+ } = {},
363
+ ): Array<{
364
+ id: number;
365
+ type: string;
366
+ title: string;
367
+ content: string;
368
+ score: number;
369
+ source: "observation" | "distillation";
370
+ created_at: string;
371
+ }> {
372
+ const db = getMemoryDB();
373
+ const budget = options.tokenBudget ?? MEMORY_CONFIG.injection.tokenBudget;
374
+ const minScore = options.minScore ?? MEMORY_CONFIG.injection.minScore;
375
+ const limit = options.limit ?? 50;
376
+
377
+ if (queryTerms.length === 0) return [];
378
+
379
+ const ftsQuery = queryTerms
380
+ .map((term) => `"${term.replace(/['"]/g, '""')}"*`)
381
+ .join(" OR ");
382
+
383
+ const results: Array<{
384
+ id: number;
385
+ type: string;
386
+ title: string;
387
+ content: string;
388
+ score: number;
389
+ source: "observation" | "distillation";
390
+ created_at: string;
391
+ }> = [];
392
+
393
+ // Search observations
394
+ try {
395
+ const obsResults = db
396
+ .query(
397
+ `
398
+ SELECT o.id, o.type, o.title,
399
+ COALESCE(o.narrative, o.title) as content,
400
+ bm25(observations_fts) as bm25_score,
401
+ o.confidence, o.created_at_epoch, o.created_at
402
+ FROM observations o
403
+ JOIN observations_fts fts ON fts.rowid = o.id
404
+ WHERE observations_fts MATCH ?
405
+ AND o.superseded_by IS NULL
406
+ ORDER BY bm25_score
407
+ LIMIT ?
408
+ `,
409
+ )
410
+ .all(ftsQuery, limit) as Array<{
411
+ id: number;
412
+ type: string;
413
+ title: string;
414
+ content: string;
415
+ bm25_score: number;
416
+ confidence: string;
417
+ created_at_epoch: number;
418
+ created_at: string;
419
+ }>;
420
+
421
+ const now = Date.now();
422
+ for (const row of obsResults) {
423
+ // Combine BM25 with recency and confidence
424
+ const ageHours = (now - row.created_at_epoch) / (1000 * 60 * 60);
425
+ const recencyFactor =
426
+ MEMORY_CONFIG.injection.recencyDecay ** (ageHours / 24);
427
+ const confidenceWeight =
428
+ row.confidence === "high"
429
+ ? 1.0
430
+ : row.confidence === "medium"
431
+ ? 0.7
432
+ : 0.4;
433
+ // BM25 scores are negative (lower = better), so negate
434
+ const score = -row.bm25_score * recencyFactor * confidenceWeight;
435
+
436
+ if (score >= minScore) {
437
+ results.push({
438
+ id: row.id,
439
+ type: row.type,
440
+ title: row.title,
441
+ content: row.content,
442
+ score,
443
+ source: "observation",
444
+ created_at: row.created_at,
445
+ });
446
+ }
447
+ }
448
+ } catch {
449
+ // FTS5 query failed
450
+ }
451
+
452
+ // Search distillations
453
+ try {
454
+ const distResults = db
455
+ .query(
456
+ `
457
+ SELECT d.id, d.content,
458
+ bm25(distillations_fts) as bm25_score,
459
+ d.time_created, d.created_at
460
+ FROM distillations d
461
+ JOIN distillations_fts fts ON fts.rowid = d.id
462
+ WHERE distillations_fts MATCH ?
463
+ ORDER BY bm25_score
464
+ LIMIT ?
465
+ `,
466
+ )
467
+ .all(ftsQuery, limit) as Array<{
468
+ id: number;
469
+ content: string;
470
+ bm25_score: number;
471
+ time_created: number;
472
+ created_at: string;
473
+ }>;
474
+
475
+ const now = Date.now();
476
+ for (const row of distResults) {
477
+ const ageHours = (now - row.time_created) / (1000 * 60 * 60);
478
+ const recencyFactor =
479
+ MEMORY_CONFIG.injection.recencyDecay ** (ageHours / 24);
480
+ const score = -row.bm25_score * recencyFactor;
481
+
482
+ if (score >= minScore) {
483
+ results.push({
484
+ id: row.id,
485
+ type: "distillation",
486
+ title: `Session distillation`,
487
+ content: row.content,
488
+ score,
489
+ source: "distillation",
490
+ created_at: row.created_at,
491
+ });
492
+ }
493
+ }
494
+ } catch {
495
+ // FTS5 query failed
496
+ }
497
+
498
+ // Sort by score descending, then greedy-pack within token budget
499
+ results.sort((a, b) => b.score - a.score);
500
+
501
+ const packed: typeof results = [];
502
+ let usedTokens = 0;
503
+
504
+ for (const item of results) {
505
+ const itemTokens = estimateTokens(item.content);
506
+ if (usedTokens + itemTokens <= budget) {
507
+ packed.push(item);
508
+ usedTokens += itemTokens;
509
+ }
510
+ }
511
+
512
+ return packed;
513
+ }
514
+
515
+ /**
516
+ * Rough token estimation (~4 chars per token).
517
+ */
518
+ export function estimateTokens(text: string): number {
519
+ return Math.ceil(text.length / 4);
520
+ }