opencode-memory-plugin 0.1.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.
- package/README.md +73 -0
- package/dist/compression/compressor.d.ts +86 -0
- package/dist/compression/compressor.d.ts.map +1 -0
- package/dist/compression/compressor.js +142 -0
- package/dist/compression/compressor.js.map +1 -0
- package/dist/compression/parser.d.ts +73 -0
- package/dist/compression/parser.d.ts.map +1 -0
- package/dist/compression/parser.js +139 -0
- package/dist/compression/parser.js.map +1 -0
- package/dist/compression/pipeline.d.ts +73 -0
- package/dist/compression/pipeline.d.ts.map +1 -0
- package/dist/compression/pipeline.js +205 -0
- package/dist/compression/pipeline.js.map +1 -0
- package/dist/compression/privacy.d.ts +8 -0
- package/dist/compression/privacy.d.ts.map +1 -0
- package/dist/compression/privacy.js +30 -0
- package/dist/compression/privacy.js.map +1 -0
- package/dist/compression/prompts.d.ts +24 -0
- package/dist/compression/prompts.d.ts.map +1 -0
- package/dist/compression/prompts.js +106 -0
- package/dist/compression/prompts.js.map +1 -0
- package/dist/compression/quality.d.ts +48 -0
- package/dist/compression/quality.d.ts.map +1 -0
- package/dist/compression/quality.js +159 -0
- package/dist/compression/quality.js.map +1 -0
- package/dist/config.d.ts +114 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +265 -0
- package/dist/config.js.map +1 -0
- package/dist/context/generator.d.ts +28 -0
- package/dist/context/generator.d.ts.map +1 -0
- package/dist/context/generator.js +80 -0
- package/dist/context/generator.js.map +1 -0
- package/dist/hooks/chat-message.d.ts +14 -0
- package/dist/hooks/chat-message.d.ts.map +1 -0
- package/dist/hooks/chat-message.js +35 -0
- package/dist/hooks/chat-message.js.map +1 -0
- package/dist/hooks/compaction.d.ts +13 -0
- package/dist/hooks/compaction.d.ts.map +1 -0
- package/dist/hooks/compaction.js +22 -0
- package/dist/hooks/compaction.js.map +1 -0
- package/dist/hooks/events.d.ts +52 -0
- package/dist/hooks/events.d.ts.map +1 -0
- package/dist/hooks/events.js +138 -0
- package/dist/hooks/events.js.map +1 -0
- package/dist/hooks/system-transform.d.ts +14 -0
- package/dist/hooks/system-transform.d.ts.map +1 -0
- package/dist/hooks/system-transform.js +26 -0
- package/dist/hooks/system-transform.js.map +1 -0
- package/dist/hooks/tool-after.d.ts +26 -0
- package/dist/hooks/tool-after.d.ts.map +1 -0
- package/dist/hooks/tool-after.js +88 -0
- package/dist/hooks/tool-after.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +79 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.d.ts +60 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +91 -0
- package/dist/logger.js.map +1 -0
- package/dist/storage/db.d.ts +22 -0
- package/dist/storage/db.d.ts.map +1 -0
- package/dist/storage/db.js +198 -0
- package/dist/storage/db.js.map +1 -0
- package/dist/storage/schema.d.ts +2473 -0
- package/dist/storage/schema.d.ts.map +1 -0
- package/dist/storage/schema.js +100 -0
- package/dist/storage/schema.js.map +1 -0
- package/dist/storage/store.d.ts +376 -0
- package/dist/storage/store.d.ts.map +1 -0
- package/dist/storage/store.js +1025 -0
- package/dist/storage/store.js.map +1 -0
- package/dist/tools/memory-forget.d.ts +11 -0
- package/dist/tools/memory-forget.d.ts.map +1 -0
- package/dist/tools/memory-forget.js +249 -0
- package/dist/tools/memory-forget.js.map +1 -0
- package/dist/tools/memory-get.d.ts +10 -0
- package/dist/tools/memory-get.d.ts.map +1 -0
- package/dist/tools/memory-get.js +50 -0
- package/dist/tools/memory-get.js.map +1 -0
- package/dist/tools/memory-search.d.ts +11 -0
- package/dist/tools/memory-search.d.ts.map +1 -0
- package/dist/tools/memory-search.js +38 -0
- package/dist/tools/memory-search.js.map +1 -0
- package/dist/tools/memory-stats.d.ts +39 -0
- package/dist/tools/memory-stats.d.ts.map +1 -0
- package/dist/tools/memory-stats.js +121 -0
- package/dist/tools/memory-stats.js.map +1 -0
- package/dist/tools/memory-timeline.d.ts +10 -0
- package/dist/tools/memory-timeline.d.ts.map +1 -0
- package/dist/tools/memory-timeline.js +49 -0
- package/dist/tools/memory-timeline.js.map +1 -0
- package/dist/types.d.ts +178 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +4 -0
- package/dist/types.js.map +1 -0
- package/dist/utils.d.ts +130 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +308 -0
- package/dist/utils.js.map +1 -0
- package/package.json +36 -0
|
@@ -0,0 +1,1025 @@
|
|
|
1
|
+
import { and, desc, eq, inArray, lte, gte, sql } from "drizzle-orm";
|
|
2
|
+
import { deletionLog, observations, pendingMessages, sessionSummaries, toolUsageStats, userPrompts, } from "./schema";
|
|
3
|
+
import { createSortableId, parseJsonValue, sanitizeFtsQuery, serializeJson } from "../utils";
|
|
4
|
+
/**
|
|
5
|
+
* Provides project-scoped persistence and retrieval operations for the memory plugin.
|
|
6
|
+
*/
|
|
7
|
+
export class MemoryStore {
|
|
8
|
+
database;
|
|
9
|
+
scope;
|
|
10
|
+
now;
|
|
11
|
+
constructor(database, scope, now) {
|
|
12
|
+
this.database = database;
|
|
13
|
+
this.scope = scope;
|
|
14
|
+
this.now = now;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Closes the underlying SQLite connection.
|
|
18
|
+
*
|
|
19
|
+
* @returns Nothing.
|
|
20
|
+
*/
|
|
21
|
+
close() {
|
|
22
|
+
this.database.sqlite.close();
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Persists a compressed observation.
|
|
26
|
+
*
|
|
27
|
+
* @param observation - Observation to save.
|
|
28
|
+
* @returns A promise that resolves after insertion.
|
|
29
|
+
*/
|
|
30
|
+
async saveObservation(observation) {
|
|
31
|
+
this.database.db.insert(observations).values({
|
|
32
|
+
id: observation.id,
|
|
33
|
+
projectId: observation.projectId,
|
|
34
|
+
projectRoot: observation.projectRoot,
|
|
35
|
+
sessionId: observation.sessionId,
|
|
36
|
+
type: observation.type,
|
|
37
|
+
title: observation.title,
|
|
38
|
+
subtitle: observation.subtitle,
|
|
39
|
+
narrative: observation.narrative,
|
|
40
|
+
facts: serializeJson(observation.facts),
|
|
41
|
+
concepts: serializeJson(observation.concepts),
|
|
42
|
+
filesInvolved: serializeJson(observation.filesInvolved),
|
|
43
|
+
rawTokenCount: observation.rawTokenCount,
|
|
44
|
+
compressedTokenCount: observation.compressedTokenCount,
|
|
45
|
+
toolName: observation.toolName,
|
|
46
|
+
modelUsed: observation.modelUsed,
|
|
47
|
+
quality: observation.quality,
|
|
48
|
+
rawFallback: observation.rawFallback,
|
|
49
|
+
createdAt: observation.createdAt,
|
|
50
|
+
}).run();
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Retrieves a single observation by identifier.
|
|
54
|
+
*
|
|
55
|
+
* @param id - Observation identifier.
|
|
56
|
+
* @returns The observation or null.
|
|
57
|
+
*/
|
|
58
|
+
async getObservation(id) {
|
|
59
|
+
const row = this.database.db
|
|
60
|
+
.select()
|
|
61
|
+
.from(observations)
|
|
62
|
+
.where(and(eq(observations.id, id), eq(observations.projectId, this.scope.projectId)))
|
|
63
|
+
.get();
|
|
64
|
+
return row ? mapObservation(row) : null;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Retrieves multiple observations in a single query.
|
|
68
|
+
*
|
|
69
|
+
* @param ids - Observation identifiers.
|
|
70
|
+
* @returns Matching observations.
|
|
71
|
+
*/
|
|
72
|
+
async getObservationsBatch(ids) {
|
|
73
|
+
if (!ids.length) {
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
const rows = this.database.db
|
|
77
|
+
.select()
|
|
78
|
+
.from(observations)
|
|
79
|
+
.where(and(inArray(observations.id, ids), eq(observations.projectId, this.scope.projectId)))
|
|
80
|
+
.orderBy(desc(observations.createdAt))
|
|
81
|
+
.all();
|
|
82
|
+
return rows.map(mapObservation);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Retrieves the most recent observations for the current project.
|
|
86
|
+
*
|
|
87
|
+
* @param limit - Maximum number of rows.
|
|
88
|
+
* @returns Recent observations ordered from newest to oldest.
|
|
89
|
+
*/
|
|
90
|
+
async getRecentObservations(limit) {
|
|
91
|
+
const rows = this.database.db
|
|
92
|
+
.select()
|
|
93
|
+
.from(observations)
|
|
94
|
+
.where(eq(observations.projectId, this.scope.projectId))
|
|
95
|
+
.orderBy(desc(observations.createdAt))
|
|
96
|
+
.limit(limit)
|
|
97
|
+
.all();
|
|
98
|
+
return rows.map(mapObservation);
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Retrieves all observations recorded for a session.
|
|
102
|
+
*
|
|
103
|
+
* @param sessionId - OpenCode session identifier.
|
|
104
|
+
* @returns Observations ordered from oldest to newest.
|
|
105
|
+
*/
|
|
106
|
+
async getSessionObservations(sessionId) {
|
|
107
|
+
const rows = this.database.db
|
|
108
|
+
.select()
|
|
109
|
+
.from(observations)
|
|
110
|
+
.where(and(eq(observations.projectId, this.scope.projectId), eq(observations.sessionId, sessionId)))
|
|
111
|
+
.orderBy(observations.createdAt)
|
|
112
|
+
.all();
|
|
113
|
+
return rows.map(mapObservation);
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Returns the total number of stored observations for the current project.
|
|
117
|
+
*
|
|
118
|
+
* @returns Observation count.
|
|
119
|
+
*/
|
|
120
|
+
async countObservations() {
|
|
121
|
+
const row = this.database.db
|
|
122
|
+
.select({ value: sql `count(*)` })
|
|
123
|
+
.from(observations)
|
|
124
|
+
.where(eq(observations.projectId, this.scope.projectId))
|
|
125
|
+
.get();
|
|
126
|
+
return row?.value ?? 0;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Adds a pending raw tool result to the crash-safe queue.
|
|
130
|
+
*
|
|
131
|
+
* @param pendingMessage - Pending message payload.
|
|
132
|
+
* @returns A promise that resolves after insertion.
|
|
133
|
+
*/
|
|
134
|
+
async enqueuePending(pendingMessage) {
|
|
135
|
+
this.database.db.insert(pendingMessages).values({
|
|
136
|
+
id: pendingMessage.id,
|
|
137
|
+
projectId: pendingMessage.projectId,
|
|
138
|
+
projectRoot: pendingMessage.projectRoot,
|
|
139
|
+
sessionId: pendingMessage.sessionId,
|
|
140
|
+
toolName: pendingMessage.toolName,
|
|
141
|
+
title: pendingMessage.title,
|
|
142
|
+
rawContent: pendingMessage.rawContent,
|
|
143
|
+
rawMetadata: pendingMessage.rawMetadata ? serializeJson(pendingMessage.rawMetadata) : null,
|
|
144
|
+
status: pendingMessage.status,
|
|
145
|
+
retryCount: pendingMessage.retryCount,
|
|
146
|
+
errorMessage: pendingMessage.errorMessage,
|
|
147
|
+
createdAt: pendingMessage.createdAt,
|
|
148
|
+
processedAt: pendingMessage.processedAt,
|
|
149
|
+
}).run();
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Fetches pending messages by status.
|
|
153
|
+
*
|
|
154
|
+
* @param statuses - Accepted queue statuses.
|
|
155
|
+
* @param limit - Maximum number of rows.
|
|
156
|
+
* @returns Matching pending messages.
|
|
157
|
+
*/
|
|
158
|
+
async getPendingMessages(statuses, limit) {
|
|
159
|
+
const rows = this.database.db
|
|
160
|
+
.select()
|
|
161
|
+
.from(pendingMessages)
|
|
162
|
+
.where(and(eq(pendingMessages.projectId, this.scope.projectId), inArray(pendingMessages.status, statuses)))
|
|
163
|
+
.orderBy(pendingMessages.createdAt)
|
|
164
|
+
.limit(limit)
|
|
165
|
+
.all();
|
|
166
|
+
return rows.map(mapPendingMessage);
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Finds queue items that were left in processing state past the orphan threshold.
|
|
170
|
+
*
|
|
171
|
+
* @param orphanThresholdMs - Threshold in milliseconds.
|
|
172
|
+
* @returns Orphaned pending messages.
|
|
173
|
+
*/
|
|
174
|
+
async getOrphanedMessages(orphanThresholdMs) {
|
|
175
|
+
const cutoff = this.now() - orphanThresholdMs;
|
|
176
|
+
const rows = this.database.db
|
|
177
|
+
.select()
|
|
178
|
+
.from(pendingMessages)
|
|
179
|
+
.where(and(eq(pendingMessages.projectId, this.scope.projectId), eq(pendingMessages.status, "processing"), lte(pendingMessages.createdAt, cutoff)))
|
|
180
|
+
.orderBy(pendingMessages.createdAt)
|
|
181
|
+
.all();
|
|
182
|
+
return rows.map(mapPendingMessage);
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Updates the status for a queued message.
|
|
186
|
+
*
|
|
187
|
+
* @param id - Pending message identifier.
|
|
188
|
+
* @param status - Next queue status.
|
|
189
|
+
* @param retryCount - Updated retry count.
|
|
190
|
+
* @param errorMessage - Optional error message.
|
|
191
|
+
* @returns A promise that resolves after the update.
|
|
192
|
+
*/
|
|
193
|
+
async updatePendingStatus(id, status, retryCount, errorMessage) {
|
|
194
|
+
this.database.db
|
|
195
|
+
.update(pendingMessages)
|
|
196
|
+
.set({
|
|
197
|
+
status,
|
|
198
|
+
retryCount,
|
|
199
|
+
errorMessage,
|
|
200
|
+
processedAt: status === "processed" ? this.now() : null,
|
|
201
|
+
})
|
|
202
|
+
.where(and(eq(pendingMessages.id, id), eq(pendingMessages.projectId, this.scope.projectId)))
|
|
203
|
+
.run();
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Counts queued items for a specific session.
|
|
207
|
+
*
|
|
208
|
+
* @param sessionId - OpenCode session identifier.
|
|
209
|
+
* @returns Queue size for that session.
|
|
210
|
+
*/
|
|
211
|
+
async countPendingForSession(sessionId) {
|
|
212
|
+
const row = this.database.db
|
|
213
|
+
.select({ value: sql `count(*)` })
|
|
214
|
+
.from(pendingMessages)
|
|
215
|
+
.where(and(eq(pendingMessages.projectId, this.scope.projectId), eq(pendingMessages.sessionId, sessionId), inArray(pendingMessages.status, ["pending", "processing"])))
|
|
216
|
+
.get();
|
|
217
|
+
return row?.value ?? 0;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Saves or replaces a session summary.
|
|
221
|
+
*
|
|
222
|
+
* @param summary - Summary payload.
|
|
223
|
+
* @returns A promise that resolves after persistence.
|
|
224
|
+
*/
|
|
225
|
+
async saveSessionSummary(summary) {
|
|
226
|
+
this.database.db
|
|
227
|
+
.insert(sessionSummaries)
|
|
228
|
+
.values({
|
|
229
|
+
id: summary.id,
|
|
230
|
+
projectId: summary.projectId,
|
|
231
|
+
projectRoot: summary.projectRoot,
|
|
232
|
+
sessionId: summary.sessionId,
|
|
233
|
+
requested: summary.requested,
|
|
234
|
+
investigated: summary.investigated,
|
|
235
|
+
learned: summary.learned,
|
|
236
|
+
completed: summary.completed,
|
|
237
|
+
nextSteps: summary.nextSteps,
|
|
238
|
+
observationCount: summary.observationCount,
|
|
239
|
+
modelUsed: summary.modelUsed,
|
|
240
|
+
createdAt: summary.createdAt,
|
|
241
|
+
})
|
|
242
|
+
.onConflictDoUpdate({
|
|
243
|
+
target: [sessionSummaries.projectId, sessionSummaries.sessionId],
|
|
244
|
+
set: {
|
|
245
|
+
requested: summary.requested,
|
|
246
|
+
investigated: summary.investigated,
|
|
247
|
+
learned: summary.learned,
|
|
248
|
+
completed: summary.completed,
|
|
249
|
+
nextSteps: summary.nextSteps,
|
|
250
|
+
observationCount: summary.observationCount,
|
|
251
|
+
modelUsed: summary.modelUsed,
|
|
252
|
+
createdAt: summary.createdAt,
|
|
253
|
+
},
|
|
254
|
+
})
|
|
255
|
+
.run();
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Retrieves the latest summary for a session.
|
|
259
|
+
*
|
|
260
|
+
* @param sessionId - OpenCode session identifier.
|
|
261
|
+
* @returns The stored summary or null.
|
|
262
|
+
*/
|
|
263
|
+
async getSessionSummary(sessionId) {
|
|
264
|
+
const row = this.database.db
|
|
265
|
+
.select()
|
|
266
|
+
.from(sessionSummaries)
|
|
267
|
+
.where(and(eq(sessionSummaries.projectId, this.scope.projectId), eq(sessionSummaries.sessionId, sessionId)))
|
|
268
|
+
.get();
|
|
269
|
+
return row ? mapSessionSummary(row) : null;
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Retrieves the most recent summaries for the current project.
|
|
273
|
+
*
|
|
274
|
+
* @param limit - Maximum number of summaries.
|
|
275
|
+
* @returns Recent summaries ordered from newest to oldest.
|
|
276
|
+
*/
|
|
277
|
+
async getRecentSummaries(limit) {
|
|
278
|
+
const rows = this.database.db
|
|
279
|
+
.select()
|
|
280
|
+
.from(sessionSummaries)
|
|
281
|
+
.where(eq(sessionSummaries.projectId, this.scope.projectId))
|
|
282
|
+
.orderBy(desc(sessionSummaries.createdAt))
|
|
283
|
+
.limit(limit)
|
|
284
|
+
.all();
|
|
285
|
+
return rows.map(mapSessionSummary);
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Stores a user prompt for later summarization and retrieval.
|
|
289
|
+
*
|
|
290
|
+
* @param prompt - Prompt payload.
|
|
291
|
+
* @returns A promise that resolves after insertion.
|
|
292
|
+
*/
|
|
293
|
+
async saveUserPrompt(prompt) {
|
|
294
|
+
this.database.db
|
|
295
|
+
.insert(userPrompts)
|
|
296
|
+
.values({
|
|
297
|
+
id: prompt.id,
|
|
298
|
+
projectId: prompt.projectId,
|
|
299
|
+
projectRoot: prompt.projectRoot,
|
|
300
|
+
sessionId: prompt.sessionId,
|
|
301
|
+
messageId: prompt.messageId,
|
|
302
|
+
content: prompt.content,
|
|
303
|
+
createdAt: prompt.createdAt,
|
|
304
|
+
})
|
|
305
|
+
.onConflictDoNothing({ target: userPrompts.messageId })
|
|
306
|
+
.run();
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Returns prompts associated with a session.
|
|
310
|
+
*
|
|
311
|
+
* @param sessionId - OpenCode session identifier.
|
|
312
|
+
* @returns Session prompts ordered from oldest to newest.
|
|
313
|
+
*/
|
|
314
|
+
async getSessionUserPrompts(sessionId) {
|
|
315
|
+
const rows = this.database.db
|
|
316
|
+
.select()
|
|
317
|
+
.from(userPrompts)
|
|
318
|
+
.where(and(eq(userPrompts.projectId, this.scope.projectId), eq(userPrompts.sessionId, sessionId)))
|
|
319
|
+
.orderBy(userPrompts.createdAt)
|
|
320
|
+
.all();
|
|
321
|
+
return rows.map(mapUserPrompt);
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Searches observations through the FTS5 index.
|
|
325
|
+
*
|
|
326
|
+
* @param query - User-entered search text.
|
|
327
|
+
* @param limit - Maximum number of results.
|
|
328
|
+
* @param typeFilter - Optional observation type filter.
|
|
329
|
+
* @returns Compact search results.
|
|
330
|
+
*/
|
|
331
|
+
async searchFTS(query, limit, typeFilter) {
|
|
332
|
+
const match = sanitizeFtsQuery(query);
|
|
333
|
+
if (!match) {
|
|
334
|
+
return [];
|
|
335
|
+
}
|
|
336
|
+
const sqlText = `
|
|
337
|
+
SELECT
|
|
338
|
+
o.id,
|
|
339
|
+
o.title,
|
|
340
|
+
o.subtitle,
|
|
341
|
+
o.type,
|
|
342
|
+
o.created_at,
|
|
343
|
+
o.tool_name,
|
|
344
|
+
o.quality
|
|
345
|
+
FROM observations_fts f
|
|
346
|
+
JOIN observations o ON o.rowid = f.rowid
|
|
347
|
+
WHERE observations_fts MATCH ?
|
|
348
|
+
AND o.project_id = ?
|
|
349
|
+
${typeFilter ? "AND o.type = ?" : ""}
|
|
350
|
+
ORDER BY bm25(observations_fts), o.created_at DESC
|
|
351
|
+
LIMIT ?
|
|
352
|
+
`;
|
|
353
|
+
const parameters = typeFilter
|
|
354
|
+
? [match, this.scope.projectId, typeFilter, limit]
|
|
355
|
+
: [match, this.scope.projectId, limit];
|
|
356
|
+
const rows = this.database.sqlite.query(sqlText).all(...parameters);
|
|
357
|
+
return rows.map((row) => ({
|
|
358
|
+
id: row.id,
|
|
359
|
+
title: row.title,
|
|
360
|
+
subtitle: row.subtitle,
|
|
361
|
+
type: row.type,
|
|
362
|
+
createdAt: row.created_at,
|
|
363
|
+
toolName: row.tool_name,
|
|
364
|
+
quality: row.quality,
|
|
365
|
+
}));
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Retrieves a timeline page for the current project.
|
|
369
|
+
*
|
|
370
|
+
* @param query - Timeline filters and pagination options.
|
|
371
|
+
* @returns Timeline page with the next cursor.
|
|
372
|
+
*/
|
|
373
|
+
async getTimeline(query) {
|
|
374
|
+
const conditions = [eq(observations.projectId, this.scope.projectId)];
|
|
375
|
+
if (query.before) {
|
|
376
|
+
conditions.push(lte(observations.createdAt, query.before));
|
|
377
|
+
}
|
|
378
|
+
if (query.after) {
|
|
379
|
+
conditions.push(sql `${observations.createdAt} >= ${query.after}`);
|
|
380
|
+
}
|
|
381
|
+
if (query.sessionId) {
|
|
382
|
+
conditions.push(eq(observations.sessionId, query.sessionId));
|
|
383
|
+
}
|
|
384
|
+
const rows = this.database.db
|
|
385
|
+
.select()
|
|
386
|
+
.from(observations)
|
|
387
|
+
.where(and(...conditions))
|
|
388
|
+
.orderBy(desc(observations.createdAt))
|
|
389
|
+
.limit(query.limit + 1)
|
|
390
|
+
.all()
|
|
391
|
+
.map(mapObservation);
|
|
392
|
+
const hasMore = rows.length > query.limit;
|
|
393
|
+
const observationsPage = hasMore ? rows.slice(0, query.limit) : rows;
|
|
394
|
+
const lastObservation = observationsPage.at(-1) ?? null;
|
|
395
|
+
return {
|
|
396
|
+
observations: observationsPage,
|
|
397
|
+
nextCursor: hasMore && lastObservation ? String(lastObservation.createdAt - 1) : null,
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* Determines whether a session has new activity after its current summary.
|
|
402
|
+
*
|
|
403
|
+
* @param sessionId - OpenCode session identifier.
|
|
404
|
+
* @returns True when a summary refresh is needed.
|
|
405
|
+
*/
|
|
406
|
+
async hasSessionActivityAfterSummary(sessionId) {
|
|
407
|
+
const summary = await this.getSessionSummary(sessionId);
|
|
408
|
+
if (!summary) {
|
|
409
|
+
const observationCount = await this.countSessionObservations(sessionId);
|
|
410
|
+
const promptCount = await this.countSessionPrompts(sessionId);
|
|
411
|
+
return observationCount > 0 || promptCount > 0;
|
|
412
|
+
}
|
|
413
|
+
const lastObservation = this.getLastTimestamp(this.database.sqlite, "observations", sessionId, this.scope.projectId);
|
|
414
|
+
const lastPrompt = this.getLastTimestamp(this.database.sqlite, "user_prompts", sessionId, this.scope.projectId);
|
|
415
|
+
const lastActivity = Math.max(lastObservation, lastPrompt);
|
|
416
|
+
return lastActivity > summary.createdAt;
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Counts observations for a session.
|
|
420
|
+
*
|
|
421
|
+
* @param sessionId - OpenCode session identifier.
|
|
422
|
+
* @returns Observation count.
|
|
423
|
+
*/
|
|
424
|
+
async countSessionObservations(sessionId) {
|
|
425
|
+
const row = this.database.db
|
|
426
|
+
.select({ value: sql `count(*)` })
|
|
427
|
+
.from(observations)
|
|
428
|
+
.where(and(eq(observations.projectId, this.scope.projectId), eq(observations.sessionId, sessionId)))
|
|
429
|
+
.get();
|
|
430
|
+
return row?.value ?? 0;
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Counts prompts for a session.
|
|
434
|
+
*
|
|
435
|
+
* @param sessionId - OpenCode session identifier.
|
|
436
|
+
* @returns Prompt count.
|
|
437
|
+
*/
|
|
438
|
+
async countSessionPrompts(sessionId) {
|
|
439
|
+
const row = this.database.db
|
|
440
|
+
.select({ value: sql `count(*)` })
|
|
441
|
+
.from(userPrompts)
|
|
442
|
+
.where(and(eq(userPrompts.projectId, this.scope.projectId), eq(userPrompts.sessionId, sessionId)))
|
|
443
|
+
.get();
|
|
444
|
+
return row?.value ?? 0;
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Deletes a set of observations by identifier.
|
|
448
|
+
*
|
|
449
|
+
* @param ids - Observation identifiers.
|
|
450
|
+
* @returns Number of deleted observations.
|
|
451
|
+
*/
|
|
452
|
+
async deleteObservations(ids) {
|
|
453
|
+
if (!ids.length) {
|
|
454
|
+
return 0;
|
|
455
|
+
}
|
|
456
|
+
const row = this.database.db
|
|
457
|
+
.select({ value: sql `count(*)` })
|
|
458
|
+
.from(observations)
|
|
459
|
+
.where(and(eq(observations.projectId, this.scope.projectId), inArray(observations.id, ids)))
|
|
460
|
+
.get();
|
|
461
|
+
const count = row?.value ?? 0;
|
|
462
|
+
if (!count) {
|
|
463
|
+
return 0;
|
|
464
|
+
}
|
|
465
|
+
this.database.db
|
|
466
|
+
.delete(observations)
|
|
467
|
+
.where(and(eq(observations.projectId, this.scope.projectId), inArray(observations.id, ids)))
|
|
468
|
+
.run();
|
|
469
|
+
return count;
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* Deletes observations that match an FTS query.
|
|
473
|
+
*
|
|
474
|
+
* @param ftsQuery - Raw user query.
|
|
475
|
+
* @returns Number of deleted observations.
|
|
476
|
+
*/
|
|
477
|
+
async deleteByQuery(ftsQuery) {
|
|
478
|
+
const match = sanitizeFtsQuery(ftsQuery);
|
|
479
|
+
if (!match) {
|
|
480
|
+
return 0;
|
|
481
|
+
}
|
|
482
|
+
const rows = this.database.sqlite
|
|
483
|
+
.query(`
|
|
484
|
+
SELECT o.id
|
|
485
|
+
FROM observations_fts f
|
|
486
|
+
JOIN observations o ON o.rowid = f.rowid
|
|
487
|
+
WHERE observations_fts MATCH ?
|
|
488
|
+
AND o.project_id = ?
|
|
489
|
+
`)
|
|
490
|
+
.all(match, this.scope.projectId);
|
|
491
|
+
return this.deleteObservations(rows.map((row) => row.id));
|
|
492
|
+
}
|
|
493
|
+
/**
|
|
494
|
+
* Deletes all observations and summary for a session.
|
|
495
|
+
*
|
|
496
|
+
* @param sessionId - OpenCode session identifier.
|
|
497
|
+
* @returns Number of deleted observations.
|
|
498
|
+
*/
|
|
499
|
+
async deleteBySession(sessionId) {
|
|
500
|
+
const row = this.database.db
|
|
501
|
+
.select({ value: sql `count(*)` })
|
|
502
|
+
.from(observations)
|
|
503
|
+
.where(and(eq(observations.projectId, this.scope.projectId), eq(observations.sessionId, sessionId)))
|
|
504
|
+
.get();
|
|
505
|
+
const count = row?.value ?? 0;
|
|
506
|
+
this.database.db
|
|
507
|
+
.delete(observations)
|
|
508
|
+
.where(and(eq(observations.projectId, this.scope.projectId), eq(observations.sessionId, sessionId)))
|
|
509
|
+
.run();
|
|
510
|
+
this.database.db
|
|
511
|
+
.delete(sessionSummaries)
|
|
512
|
+
.where(and(eq(sessionSummaries.projectId, this.scope.projectId), eq(sessionSummaries.sessionId, sessionId)))
|
|
513
|
+
.run();
|
|
514
|
+
return count;
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* Deletes observations created before or at a given date.
|
|
518
|
+
*
|
|
519
|
+
* @param date - Cutoff date.
|
|
520
|
+
* @returns Number of deleted observations.
|
|
521
|
+
*/
|
|
522
|
+
async deleteBefore(date) {
|
|
523
|
+
const cutoff = date.getTime();
|
|
524
|
+
if (!Number.isFinite(cutoff)) {
|
|
525
|
+
return 0;
|
|
526
|
+
}
|
|
527
|
+
const row = this.database.db
|
|
528
|
+
.select({ value: sql `count(*)` })
|
|
529
|
+
.from(observations)
|
|
530
|
+
.where(and(eq(observations.projectId, this.scope.projectId), lte(observations.createdAt, cutoff)))
|
|
531
|
+
.get();
|
|
532
|
+
const count = row?.value ?? 0;
|
|
533
|
+
if (!count) {
|
|
534
|
+
return 0;
|
|
535
|
+
}
|
|
536
|
+
this.database.db
|
|
537
|
+
.delete(observations)
|
|
538
|
+
.where(and(eq(observations.projectId, this.scope.projectId), lte(observations.createdAt, cutoff)))
|
|
539
|
+
.run();
|
|
540
|
+
return count;
|
|
541
|
+
}
|
|
542
|
+
/**
|
|
543
|
+
* Stores a deletion audit log entry.
|
|
544
|
+
*
|
|
545
|
+
* @param criteria - JSON criteria description.
|
|
546
|
+
* @param count - Deleted observation count.
|
|
547
|
+
* @param initiator - Operation initiator.
|
|
548
|
+
* @returns A promise that resolves after insertion.
|
|
549
|
+
*/
|
|
550
|
+
async logDeletion(criteria, count, initiator) {
|
|
551
|
+
this.database.db.insert(deletionLog).values({
|
|
552
|
+
id: this.createId(),
|
|
553
|
+
projectId: this.scope.projectId,
|
|
554
|
+
projectRoot: this.scope.projectRoot,
|
|
555
|
+
timestamp: this.now(),
|
|
556
|
+
criteria,
|
|
557
|
+
count,
|
|
558
|
+
initiator,
|
|
559
|
+
}).run();
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* Increments tool usage counters for a session.
|
|
563
|
+
*
|
|
564
|
+
* @param sessionId - OpenCode session identifier.
|
|
565
|
+
* @param toolName - Tool name.
|
|
566
|
+
* @returns A promise that resolves after update.
|
|
567
|
+
*/
|
|
568
|
+
async incrementToolUsage(sessionId, toolName) {
|
|
569
|
+
const existing = this.database.db
|
|
570
|
+
.select({ id: toolUsageStats.id, callCount: toolUsageStats.callCount })
|
|
571
|
+
.from(toolUsageStats)
|
|
572
|
+
.where(and(eq(toolUsageStats.projectId, this.scope.projectId), eq(toolUsageStats.sessionId, sessionId), eq(toolUsageStats.toolName, toolName)))
|
|
573
|
+
.get();
|
|
574
|
+
if (!existing) {
|
|
575
|
+
this.database.db.insert(toolUsageStats).values({
|
|
576
|
+
id: this.createId(),
|
|
577
|
+
projectId: this.scope.projectId,
|
|
578
|
+
projectRoot: this.scope.projectRoot,
|
|
579
|
+
sessionId,
|
|
580
|
+
toolName,
|
|
581
|
+
callCount: 1,
|
|
582
|
+
createdAt: this.now(),
|
|
583
|
+
}).run();
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
this.database.db
|
|
587
|
+
.update(toolUsageStats)
|
|
588
|
+
.set({
|
|
589
|
+
callCount: existing.callCount + 1,
|
|
590
|
+
createdAt: this.now(),
|
|
591
|
+
})
|
|
592
|
+
.where(eq(toolUsageStats.id, existing.id))
|
|
593
|
+
.run();
|
|
594
|
+
}
|
|
595
|
+
/**
|
|
596
|
+
* Retrieves tool usage stats from the last N days.
|
|
597
|
+
*
|
|
598
|
+
* @param days - Lookback window in days.
|
|
599
|
+
* @returns Matching tool usage rows.
|
|
600
|
+
*/
|
|
601
|
+
async getToolUsageStats(days) {
|
|
602
|
+
const cutoff = this.now() - Math.max(1, days) * 86_400_000;
|
|
603
|
+
const rows = this.database.db
|
|
604
|
+
.select()
|
|
605
|
+
.from(toolUsageStats)
|
|
606
|
+
.where(and(eq(toolUsageStats.projectId, this.scope.projectId), gte(toolUsageStats.createdAt, cutoff)))
|
|
607
|
+
.orderBy(desc(toolUsageStats.createdAt))
|
|
608
|
+
.all();
|
|
609
|
+
return rows.map(mapToolUsageStat);
|
|
610
|
+
}
|
|
611
|
+
/**
|
|
612
|
+
* Returns observation quality distribution counts.
|
|
613
|
+
*
|
|
614
|
+
* @returns Quality counts by bucket.
|
|
615
|
+
*/
|
|
616
|
+
async getQualityDistribution() {
|
|
617
|
+
const rows = this.database.sqlite
|
|
618
|
+
.query(`
|
|
619
|
+
SELECT quality, COUNT(*) AS value
|
|
620
|
+
FROM observations
|
|
621
|
+
WHERE project_id = ?
|
|
622
|
+
GROUP BY quality
|
|
623
|
+
`)
|
|
624
|
+
.all(this.scope.projectId);
|
|
625
|
+
const distribution = {
|
|
626
|
+
high: 0,
|
|
627
|
+
medium: 0,
|
|
628
|
+
low: 0,
|
|
629
|
+
};
|
|
630
|
+
for (const row of rows) {
|
|
631
|
+
if (row.quality === "high" || row.quality === "medium" || row.quality === "low") {
|
|
632
|
+
distribution[row.quality] = row.value;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
return distribution;
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* Returns success metrics for a compression model.
|
|
639
|
+
*
|
|
640
|
+
* @param modelName - Model identifier.
|
|
641
|
+
* @returns Total, success and rate values.
|
|
642
|
+
*/
|
|
643
|
+
async getModelSuccessRate(modelName) {
|
|
644
|
+
const totalRow = this.database.db
|
|
645
|
+
.select({ value: sql `count(*)` })
|
|
646
|
+
.from(observations)
|
|
647
|
+
.where(and(eq(observations.projectId, this.scope.projectId), eq(observations.modelUsed, modelName)))
|
|
648
|
+
.get();
|
|
649
|
+
const successRow = this.database.db
|
|
650
|
+
.select({ value: sql `count(*)` })
|
|
651
|
+
.from(observations)
|
|
652
|
+
.where(and(eq(observations.projectId, this.scope.projectId), eq(observations.modelUsed, modelName), inArray(observations.quality, ["high", "medium"])))
|
|
653
|
+
.get();
|
|
654
|
+
const total = totalRow?.value ?? 0;
|
|
655
|
+
const success = successRow?.value ?? 0;
|
|
656
|
+
return {
|
|
657
|
+
total,
|
|
658
|
+
success,
|
|
659
|
+
rate: total > 0 ? success / total : 0,
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* Searches observations within a date range.
|
|
664
|
+
*
|
|
665
|
+
* @param from - Range start.
|
|
666
|
+
* @param to - Range end.
|
|
667
|
+
* @param limit - Maximum number of rows.
|
|
668
|
+
* @returns Matching observations.
|
|
669
|
+
*/
|
|
670
|
+
async searchByDateRange(from, to, limit) {
|
|
671
|
+
const fromTimestamp = from.getTime();
|
|
672
|
+
const toTimestamp = to.getTime();
|
|
673
|
+
if (!Number.isFinite(fromTimestamp) || !Number.isFinite(toTimestamp)) {
|
|
674
|
+
return [];
|
|
675
|
+
}
|
|
676
|
+
const start = Math.min(fromTimestamp, toTimestamp);
|
|
677
|
+
const end = Math.max(fromTimestamp, toTimestamp);
|
|
678
|
+
const rows = this.database.db
|
|
679
|
+
.select()
|
|
680
|
+
.from(observations)
|
|
681
|
+
.where(and(eq(observations.projectId, this.scope.projectId), gte(observations.createdAt, start), lte(observations.createdAt, end)))
|
|
682
|
+
.orderBy(desc(observations.createdAt))
|
|
683
|
+
.limit(Math.max(1, limit))
|
|
684
|
+
.all();
|
|
685
|
+
return rows.map(mapObservation);
|
|
686
|
+
}
|
|
687
|
+
/**
|
|
688
|
+
* Searches observations by matching file paths against the FTS index.
|
|
689
|
+
*
|
|
690
|
+
* @param filePaths - File path patterns.
|
|
691
|
+
* @returns Matching observations.
|
|
692
|
+
*/
|
|
693
|
+
async searchByFiles(filePaths) {
|
|
694
|
+
const matches = filePaths
|
|
695
|
+
.map((filePath) => sanitizeFtsQuery(filePath))
|
|
696
|
+
.filter(Boolean);
|
|
697
|
+
if (!matches.length) {
|
|
698
|
+
return [];
|
|
699
|
+
}
|
|
700
|
+
const matchQuery = matches.map((value) => `(${value})`).join(" OR ");
|
|
701
|
+
const rows = this.database.sqlite
|
|
702
|
+
.query(`
|
|
703
|
+
SELECT o.id
|
|
704
|
+
FROM observations_fts f
|
|
705
|
+
JOIN observations o ON o.rowid = f.rowid
|
|
706
|
+
WHERE observations_fts MATCH ?
|
|
707
|
+
AND o.project_id = ?
|
|
708
|
+
ORDER BY bm25(observations_fts), o.created_at DESC
|
|
709
|
+
LIMIT 200
|
|
710
|
+
`)
|
|
711
|
+
.all(matchQuery, this.scope.projectId);
|
|
712
|
+
return this.getObservationsBatch(rows.map((row) => row.id));
|
|
713
|
+
}
|
|
714
|
+
/**
|
|
715
|
+
* Returns the number of summaries for the current project.
|
|
716
|
+
*
|
|
717
|
+
* @returns Summary count.
|
|
718
|
+
*/
|
|
719
|
+
async countSessionSummaries() {
|
|
720
|
+
const row = this.database.db
|
|
721
|
+
.select({ value: sql `count(*)` })
|
|
722
|
+
.from(sessionSummaries)
|
|
723
|
+
.where(eq(sessionSummaries.projectId, this.scope.projectId))
|
|
724
|
+
.get();
|
|
725
|
+
return row?.value ?? 0;
|
|
726
|
+
}
|
|
727
|
+
/**
|
|
728
|
+
* Returns pending queue counts grouped by status.
|
|
729
|
+
*
|
|
730
|
+
* @returns Status count object.
|
|
731
|
+
*/
|
|
732
|
+
async getPendingStatusCounts() {
|
|
733
|
+
const rows = this.database.sqlite
|
|
734
|
+
.query(`
|
|
735
|
+
SELECT status, COUNT(*) AS value
|
|
736
|
+
FROM pending_messages
|
|
737
|
+
WHERE project_id = ?
|
|
738
|
+
GROUP BY status
|
|
739
|
+
`)
|
|
740
|
+
.all(this.scope.projectId);
|
|
741
|
+
const counts = {
|
|
742
|
+
pending: 0,
|
|
743
|
+
processing: 0,
|
|
744
|
+
processed: 0,
|
|
745
|
+
failed: 0,
|
|
746
|
+
};
|
|
747
|
+
for (const row of rows) {
|
|
748
|
+
if (row.status in counts) {
|
|
749
|
+
counts[row.status] = row.value;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
return counts;
|
|
753
|
+
}
|
|
754
|
+
/**
|
|
755
|
+
* Counts observations since a timestamp.
|
|
756
|
+
*
|
|
757
|
+
* @param timestamp - Lower bound timestamp.
|
|
758
|
+
* @returns Observation count.
|
|
759
|
+
*/
|
|
760
|
+
async countObservationsSince(timestamp) {
|
|
761
|
+
const row = this.database.db
|
|
762
|
+
.select({ value: sql `count(*)` })
|
|
763
|
+
.from(observations)
|
|
764
|
+
.where(and(eq(observations.projectId, this.scope.projectId), gte(observations.createdAt, timestamp)))
|
|
765
|
+
.get();
|
|
766
|
+
return row?.value ?? 0;
|
|
767
|
+
}
|
|
768
|
+
/**
|
|
769
|
+
* Calculates compression ratio and last compression timestamp.
|
|
770
|
+
*
|
|
771
|
+
* @returns Compression summary values.
|
|
772
|
+
*/
|
|
773
|
+
async getCompressionStats() {
|
|
774
|
+
const row = this.database.sqlite
|
|
775
|
+
.query(`
|
|
776
|
+
SELECT
|
|
777
|
+
AVG(CASE WHEN compressed_token_count > 0 THEN CAST(raw_token_count AS REAL) / compressed_token_count END) AS average_ratio,
|
|
778
|
+
MAX(created_at) AS last_compressed_at
|
|
779
|
+
FROM observations
|
|
780
|
+
WHERE project_id = ?
|
|
781
|
+
`)
|
|
782
|
+
.get(this.scope.projectId);
|
|
783
|
+
return {
|
|
784
|
+
averageRatio: row?.average_ratio ?? 0,
|
|
785
|
+
lastCompressedAt: row?.last_compressed_at ?? null,
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
/**
|
|
789
|
+
* Returns deletion log totals for a lookback window.
|
|
790
|
+
*
|
|
791
|
+
* @param days - Lookback in days.
|
|
792
|
+
* @returns Operation and deletion totals.
|
|
793
|
+
*/
|
|
794
|
+
async getDeletionStats(days) {
|
|
795
|
+
const cutoff = this.now() - Math.max(1, days) * 86_400_000;
|
|
796
|
+
const row = this.database.sqlite
|
|
797
|
+
.query(`
|
|
798
|
+
SELECT COUNT(*) AS operations, COALESCE(SUM(count), 0) AS removed
|
|
799
|
+
FROM deletion_log
|
|
800
|
+
WHERE project_id = ?
|
|
801
|
+
AND timestamp >= ?
|
|
802
|
+
`)
|
|
803
|
+
.get(this.scope.projectId, cutoff);
|
|
804
|
+
return {
|
|
805
|
+
operations: row?.operations ?? 0,
|
|
806
|
+
removed: row?.removed ?? 0,
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
/**
|
|
810
|
+
* Returns the current SQLite database size in bytes.
|
|
811
|
+
*
|
|
812
|
+
* @returns Database file size estimate.
|
|
813
|
+
*/
|
|
814
|
+
async getDatabaseSizeBytes() {
|
|
815
|
+
const row = this.database.sqlite
|
|
816
|
+
.query("SELECT page_count AS page_count, page_size AS page_size FROM pragma_page_count(), pragma_page_size()")
|
|
817
|
+
.get();
|
|
818
|
+
if (!row) {
|
|
819
|
+
return 0;
|
|
820
|
+
}
|
|
821
|
+
return row.page_count * row.page_size;
|
|
822
|
+
}
|
|
823
|
+
/**
|
|
824
|
+
* Returns deletion log entries from the last N days.
|
|
825
|
+
*
|
|
826
|
+
* @param days - Lookback window in days.
|
|
827
|
+
* @returns Matching deletion entries.
|
|
828
|
+
*/
|
|
829
|
+
async getDeletionLog(days) {
|
|
830
|
+
const cutoff = this.now() - Math.max(1, days) * 86_400_000;
|
|
831
|
+
const rows = this.database.db
|
|
832
|
+
.select()
|
|
833
|
+
.from(deletionLog)
|
|
834
|
+
.where(and(eq(deletionLog.projectId, this.scope.projectId), gte(deletionLog.timestamp, cutoff)))
|
|
835
|
+
.orderBy(desc(deletionLog.timestamp))
|
|
836
|
+
.all();
|
|
837
|
+
return rows.map(mapDeletionLogEntry);
|
|
838
|
+
}
|
|
839
|
+
/**
|
|
840
|
+
* Deletes data older than the configured retention windows.
|
|
841
|
+
*
|
|
842
|
+
* @param retentionDays - Number of days to keep observations and prompts.
|
|
843
|
+
* @returns A promise that resolves after cleanup.
|
|
844
|
+
*/
|
|
845
|
+
async cleanupOldData(retentionDays) {
|
|
846
|
+
const now = this.now();
|
|
847
|
+
const retentionCutoff = now - retentionDays * 86_400_000;
|
|
848
|
+
const pendingCutoff = now - 7 * 86_400_000;
|
|
849
|
+
const summaryCutoff = now - retentionDays * 2 * 86_400_000;
|
|
850
|
+
const observationDeleteRow = this.database.db
|
|
851
|
+
.select({ value: sql `count(*)` })
|
|
852
|
+
.from(observations)
|
|
853
|
+
.where(and(eq(observations.projectId, this.scope.projectId), lte(observations.createdAt, retentionCutoff)))
|
|
854
|
+
.get();
|
|
855
|
+
const observationDeleteCount = observationDeleteRow?.value ?? 0;
|
|
856
|
+
this.database.db
|
|
857
|
+
.delete(observations)
|
|
858
|
+
.where(and(eq(observations.projectId, this.scope.projectId), lte(observations.createdAt, retentionCutoff)))
|
|
859
|
+
.run();
|
|
860
|
+
if (observationDeleteCount > 0) {
|
|
861
|
+
await this.logDeletion(JSON.stringify({ type: "retention", target: "observations", before: retentionCutoff }), observationDeleteCount, "retention_cleanup");
|
|
862
|
+
}
|
|
863
|
+
this.database.db
|
|
864
|
+
.delete(userPrompts)
|
|
865
|
+
.where(and(eq(userPrompts.projectId, this.scope.projectId), lte(userPrompts.createdAt, retentionCutoff)))
|
|
866
|
+
.run();
|
|
867
|
+
this.database.db
|
|
868
|
+
.delete(sessionSummaries)
|
|
869
|
+
.where(and(eq(sessionSummaries.projectId, this.scope.projectId), lte(sessionSummaries.createdAt, summaryCutoff)))
|
|
870
|
+
.run();
|
|
871
|
+
this.database.db
|
|
872
|
+
.delete(pendingMessages)
|
|
873
|
+
.where(and(eq(pendingMessages.projectId, this.scope.projectId), inArray(pendingMessages.status, ["processed", "failed"]), lte(pendingMessages.createdAt, pendingCutoff)))
|
|
874
|
+
.run();
|
|
875
|
+
this.database.sqlite.exec("VACUUM");
|
|
876
|
+
}
|
|
877
|
+
/**
|
|
878
|
+
* Creates a new project-scoped record identifier.
|
|
879
|
+
*
|
|
880
|
+
* @returns A sortable identifier.
|
|
881
|
+
*/
|
|
882
|
+
createId() {
|
|
883
|
+
return createSortableId(this.now);
|
|
884
|
+
}
|
|
885
|
+
/**
|
|
886
|
+
* Returns the last activity timestamp for a table and session.
|
|
887
|
+
*
|
|
888
|
+
* @param sqlite - SQLite client.
|
|
889
|
+
* @param tableName - Table to inspect.
|
|
890
|
+
* @param sessionId - OpenCode session identifier.
|
|
891
|
+
* @param projectId - Current project identifier.
|
|
892
|
+
* @returns The last timestamp or zero.
|
|
893
|
+
*/
|
|
894
|
+
getLastTimestamp(sqlite, tableName, sessionId, projectId) {
|
|
895
|
+
const row = sqlite
|
|
896
|
+
.query(`SELECT MAX(created_at) AS value FROM ${tableName} WHERE project_id = ? AND session_id = ?`)
|
|
897
|
+
.get(projectId, sessionId);
|
|
898
|
+
return row?.value ?? 0;
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
/**
|
|
902
|
+
* Maps an observation row into the runtime shape.
|
|
903
|
+
*
|
|
904
|
+
* @param row - Database row.
|
|
905
|
+
* @returns Normalized observation.
|
|
906
|
+
*/
|
|
907
|
+
export function mapObservation(row) {
|
|
908
|
+
return {
|
|
909
|
+
id: row.id,
|
|
910
|
+
projectId: row.projectId,
|
|
911
|
+
projectRoot: row.projectRoot,
|
|
912
|
+
sessionId: row.sessionId,
|
|
913
|
+
type: row.type,
|
|
914
|
+
title: row.title,
|
|
915
|
+
subtitle: row.subtitle ?? null,
|
|
916
|
+
narrative: row.narrative,
|
|
917
|
+
facts: parseJsonValue(row.facts, []),
|
|
918
|
+
concepts: parseJsonValue(row.concepts, []),
|
|
919
|
+
filesInvolved: parseJsonValue(row.filesInvolved, []),
|
|
920
|
+
rawTokenCount: row.rawTokenCount,
|
|
921
|
+
compressedTokenCount: row.compressedTokenCount,
|
|
922
|
+
toolName: row.toolName ?? null,
|
|
923
|
+
modelUsed: row.modelUsed ?? null,
|
|
924
|
+
quality: row.quality ?? "high",
|
|
925
|
+
rawFallback: row.rawFallback ?? null,
|
|
926
|
+
createdAt: row.createdAt,
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
/**
|
|
930
|
+
* Maps a pending queue row into the runtime shape.
|
|
931
|
+
*
|
|
932
|
+
* @param row - Database row.
|
|
933
|
+
* @returns Normalized pending message.
|
|
934
|
+
*/
|
|
935
|
+
export function mapPendingMessage(row) {
|
|
936
|
+
return {
|
|
937
|
+
id: row.id,
|
|
938
|
+
projectId: row.projectId,
|
|
939
|
+
projectRoot: row.projectRoot,
|
|
940
|
+
sessionId: row.sessionId,
|
|
941
|
+
toolName: row.toolName,
|
|
942
|
+
title: row.title ?? null,
|
|
943
|
+
rawContent: row.rawContent,
|
|
944
|
+
rawMetadata: parseJsonValue(row.rawMetadata, null),
|
|
945
|
+
status: row.status,
|
|
946
|
+
retryCount: row.retryCount,
|
|
947
|
+
errorMessage: row.errorMessage ?? null,
|
|
948
|
+
createdAt: row.createdAt,
|
|
949
|
+
processedAt: row.processedAt ?? null,
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
/**
|
|
953
|
+
* Maps a summary row into the runtime shape.
|
|
954
|
+
*
|
|
955
|
+
* @param row - Database row.
|
|
956
|
+
* @returns Normalized session summary.
|
|
957
|
+
*/
|
|
958
|
+
export function mapSessionSummary(row) {
|
|
959
|
+
return {
|
|
960
|
+
id: row.id,
|
|
961
|
+
projectId: row.projectId,
|
|
962
|
+
projectRoot: row.projectRoot,
|
|
963
|
+
sessionId: row.sessionId,
|
|
964
|
+
requested: row.requested ?? null,
|
|
965
|
+
investigated: row.investigated ?? null,
|
|
966
|
+
learned: row.learned ?? null,
|
|
967
|
+
completed: row.completed ?? null,
|
|
968
|
+
nextSteps: row.nextSteps ?? null,
|
|
969
|
+
observationCount: row.observationCount,
|
|
970
|
+
modelUsed: row.modelUsed ?? null,
|
|
971
|
+
createdAt: row.createdAt,
|
|
972
|
+
};
|
|
973
|
+
}
|
|
974
|
+
/**
|
|
975
|
+
* Maps a prompt row into the runtime shape.
|
|
976
|
+
*
|
|
977
|
+
* @param row - Database row.
|
|
978
|
+
* @returns Normalized user prompt.
|
|
979
|
+
*/
|
|
980
|
+
export function mapUserPrompt(row) {
|
|
981
|
+
return {
|
|
982
|
+
id: row.id,
|
|
983
|
+
projectId: row.projectId,
|
|
984
|
+
projectRoot: row.projectRoot,
|
|
985
|
+
sessionId: row.sessionId,
|
|
986
|
+
messageId: row.messageId,
|
|
987
|
+
content: row.content,
|
|
988
|
+
createdAt: row.createdAt,
|
|
989
|
+
};
|
|
990
|
+
}
|
|
991
|
+
/**
|
|
992
|
+
* Maps a deletion log row into the runtime shape.
|
|
993
|
+
*
|
|
994
|
+
* @param row - Database row.
|
|
995
|
+
* @returns Normalized deletion log entry.
|
|
996
|
+
*/
|
|
997
|
+
export function mapDeletionLogEntry(row) {
|
|
998
|
+
return {
|
|
999
|
+
id: row.id,
|
|
1000
|
+
projectId: row.projectId,
|
|
1001
|
+
projectRoot: row.projectRoot,
|
|
1002
|
+
timestamp: row.timestamp,
|
|
1003
|
+
criteria: row.criteria,
|
|
1004
|
+
count: row.count,
|
|
1005
|
+
initiator: row.initiator,
|
|
1006
|
+
};
|
|
1007
|
+
}
|
|
1008
|
+
/**
|
|
1009
|
+
* Maps a tool usage stats row into the runtime shape.
|
|
1010
|
+
*
|
|
1011
|
+
* @param row - Database row.
|
|
1012
|
+
* @returns Normalized tool usage stats entry.
|
|
1013
|
+
*/
|
|
1014
|
+
export function mapToolUsageStat(row) {
|
|
1015
|
+
return {
|
|
1016
|
+
id: row.id,
|
|
1017
|
+
projectId: row.projectId,
|
|
1018
|
+
projectRoot: row.projectRoot,
|
|
1019
|
+
sessionId: row.sessionId,
|
|
1020
|
+
toolName: row.toolName,
|
|
1021
|
+
callCount: row.callCount,
|
|
1022
|
+
createdAt: row.createdAt,
|
|
1023
|
+
};
|
|
1024
|
+
}
|
|
1025
|
+
//# sourceMappingURL=store.js.map
|