@massu/core 0.1.1 → 0.1.2
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 +2 -2
- package/dist/hooks/cost-tracker.js +23 -35
- package/dist/hooks/post-edit-context.js +2 -2
- package/dist/hooks/post-tool-use.js +43 -58
- package/dist/hooks/pre-compact.js +23 -38
- package/dist/hooks/pre-delete-check.js +18 -31
- package/dist/hooks/quality-event.js +23 -35
- package/dist/hooks/session-end.js +62 -78
- package/dist/hooks/session-start.js +33 -42
- package/dist/hooks/user-prompt.js +23 -38
- package/package.json +8 -14
- package/src/adr-generator.ts +9 -2
- package/src/analytics.ts +9 -3
- package/src/audit-trail.ts +10 -3
- package/src/cloud-sync.ts +14 -18
- package/src/commands/init.ts +1 -5
- package/src/cost-tracker.ts +11 -6
- package/src/dependency-scorer.ts +9 -2
- package/src/docs-tools.ts +13 -10
- package/src/hooks/post-edit-context.ts +3 -3
- package/src/hooks/session-end.ts +3 -3
- package/src/hooks/session-start.ts +2 -2
- package/src/memory-db.ts +1351 -23
- package/src/memory-tools.ts +14 -15
- package/src/observability-tools.ts +13 -2
- package/src/prompt-analyzer.ts +9 -2
- package/src/regression-detector.ts +9 -3
- package/src/security-scorer.ts +9 -2
- package/src/sentinel-db.ts +43 -88
- package/src/sentinel-tools.ts +8 -11
- package/src/server.ts +1 -2
- package/src/team-knowledge.ts +9 -2
- package/src/tools.ts +771 -35
- package/src/validate-features-runner.ts +0 -1
- package/src/validation-engine.ts +9 -2
- package/dist/cli.js +0 -7890
- package/dist/server.js +0 -7008
- package/src/__tests__/adr-generator.test.ts +0 -260
- package/src/__tests__/analytics.test.ts +0 -282
- package/src/__tests__/audit-trail.test.ts +0 -382
- package/src/__tests__/backfill-sessions.test.ts +0 -690
- package/src/__tests__/cli.test.ts +0 -290
- package/src/__tests__/cloud-sync.test.ts +0 -261
- package/src/__tests__/config-sections.test.ts +0 -359
- package/src/__tests__/config.test.ts +0 -732
- package/src/__tests__/cost-tracker.test.ts +0 -348
- package/src/__tests__/db.test.ts +0 -177
- package/src/__tests__/dependency-scorer.test.ts +0 -325
- package/src/__tests__/docs-integration.test.ts +0 -178
- package/src/__tests__/docs-tools.test.ts +0 -199
- package/src/__tests__/domains.test.ts +0 -236
- package/src/__tests__/hooks.test.ts +0 -221
- package/src/__tests__/import-resolver.test.ts +0 -95
- package/src/__tests__/integration/path-traversal.test.ts +0 -134
- package/src/__tests__/integration/pricing-consistency.test.ts +0 -88
- package/src/__tests__/integration/tool-registration.test.ts +0 -146
- package/src/__tests__/memory-db.test.ts +0 -404
- package/src/__tests__/memory-enhancements.test.ts +0 -316
- package/src/__tests__/memory-tools.test.ts +0 -199
- package/src/__tests__/middleware-tree.test.ts +0 -177
- package/src/__tests__/observability-tools.test.ts +0 -595
- package/src/__tests__/observability.test.ts +0 -437
- package/src/__tests__/observation-extractor.test.ts +0 -167
- package/src/__tests__/page-deps.test.ts +0 -60
- package/src/__tests__/prompt-analyzer.test.ts +0 -298
- package/src/__tests__/regression-detector.test.ts +0 -295
- package/src/__tests__/rules.test.ts +0 -87
- package/src/__tests__/schema-mapper.test.ts +0 -29
- package/src/__tests__/security-scorer.test.ts +0 -238
- package/src/__tests__/security-utils.test.ts +0 -175
- package/src/__tests__/sentinel-db.test.ts +0 -491
- package/src/__tests__/sentinel-scanner.test.ts +0 -750
- package/src/__tests__/sentinel-tools.test.ts +0 -324
- package/src/__tests__/sentinel-types.test.ts +0 -750
- package/src/__tests__/server.test.ts +0 -452
- package/src/__tests__/session-archiver.test.ts +0 -524
- package/src/__tests__/session-state-generator.test.ts +0 -900
- package/src/__tests__/team-knowledge.test.ts +0 -327
- package/src/__tests__/tools.test.ts +0 -340
- package/src/__tests__/transcript-parser.test.ts +0 -195
- package/src/__tests__/trpc-index.test.ts +0 -25
- package/src/__tests__/validate-features-runner.test.ts +0 -517
- package/src/__tests__/validation-engine.test.ts +0 -300
- package/src/core-tools.ts +0 -685
- package/src/memory-queries.ts +0 -804
- package/src/memory-schema.ts +0 -546
- package/src/tool-helpers.ts +0 -41
package/src/memory-queries.ts
DELETED
|
@@ -1,804 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2026 Massu. All rights reserved.
|
|
2
|
-
// Licensed under BSL 1.1 - see LICENSE file for details.
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Memory database CRUD query functions.
|
|
6
|
-
* Split from memory-db.ts (P3-001 remediation) to keep memory-db.ts
|
|
7
|
-
* focused on connection factory + schema initialization.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import type Database from 'better-sqlite3';
|
|
11
|
-
import { basename } from 'path';
|
|
12
|
-
|
|
13
|
-
// ============================================================
|
|
14
|
-
// Cloud Sync: Queue Functions
|
|
15
|
-
// ============================================================
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Enqueue a sync payload for later retry.
|
|
19
|
-
*/
|
|
20
|
-
export function enqueueSyncPayload(db: Database.Database, payload: string): void {
|
|
21
|
-
db.prepare('INSERT INTO pending_sync (payload) VALUES (?)').run(payload);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Dequeue pending sync items (oldest first).
|
|
26
|
-
* Items with retry_count >= 10 are silently discarded to prevent infinite accumulation.
|
|
27
|
-
*/
|
|
28
|
-
export function dequeuePendingSync(
|
|
29
|
-
db: Database.Database,
|
|
30
|
-
limit: number = 10
|
|
31
|
-
): Array<{ id: number; payload: string; retry_count: number }> {
|
|
32
|
-
// First, discard items that have exceeded max retries
|
|
33
|
-
db.prepare('DELETE FROM pending_sync WHERE retry_count >= 10').run();
|
|
34
|
-
|
|
35
|
-
return db.prepare(
|
|
36
|
-
'SELECT id, payload, retry_count FROM pending_sync ORDER BY created_at ASC LIMIT ?'
|
|
37
|
-
).all(limit) as Array<{ id: number; payload: string; retry_count: number }>;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Remove a successfully synced item from the queue.
|
|
42
|
-
*/
|
|
43
|
-
export function removePendingSync(db: Database.Database, id: number): void {
|
|
44
|
-
db.prepare('DELETE FROM pending_sync WHERE id = ?').run(id);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Increment retry count and record the last error for a failed sync attempt.
|
|
49
|
-
*/
|
|
50
|
-
export function incrementRetryCount(db: Database.Database, id: number, error: string): void {
|
|
51
|
-
db.prepare(
|
|
52
|
-
'UPDATE pending_sync SET retry_count = retry_count + 1, last_error = ? WHERE id = ?'
|
|
53
|
-
).run(error, id);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// ============================================================
|
|
57
|
-
// P1-002: Database Access Functions
|
|
58
|
-
// ============================================================
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Auto-assign importance score based on observation type and optional VR result.
|
|
62
|
-
* Scale: 5=decision/failed_attempt, 4=cr_violation/vr_check(FAIL),
|
|
63
|
-
* 3=feature/bugfix, 2=vr_check(PASS)/refactor, 1=file_change/discovery
|
|
64
|
-
*/
|
|
65
|
-
export function assignImportance(type: string, vrResult?: string): number {
|
|
66
|
-
switch (type) {
|
|
67
|
-
case 'decision':
|
|
68
|
-
case 'failed_attempt':
|
|
69
|
-
return 5;
|
|
70
|
-
case 'cr_violation':
|
|
71
|
-
case 'incident_near_miss':
|
|
72
|
-
return 4;
|
|
73
|
-
case 'vr_check':
|
|
74
|
-
return vrResult === 'PASS' ? 2 : 4;
|
|
75
|
-
case 'pattern_compliance':
|
|
76
|
-
return vrResult === 'PASS' ? 2 : 4;
|
|
77
|
-
case 'feature':
|
|
78
|
-
case 'bugfix':
|
|
79
|
-
return 3;
|
|
80
|
-
case 'refactor':
|
|
81
|
-
return 2;
|
|
82
|
-
case 'file_change':
|
|
83
|
-
case 'discovery':
|
|
84
|
-
return 1;
|
|
85
|
-
default:
|
|
86
|
-
return 3;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Derive task_id from plan file path.
|
|
92
|
-
* Sessions working on the same plan file share a task_id.
|
|
93
|
-
*/
|
|
94
|
-
export function autoDetectTaskId(planFile: string | null | undefined): string | null {
|
|
95
|
-
if (!planFile) return null;
|
|
96
|
-
// Use the plan filename without extension as task_id
|
|
97
|
-
// e.g., "/path/to/2026-01-30-massu-memory.md" -> "2026-01-30-massu-memory"
|
|
98
|
-
const base = basename(planFile);
|
|
99
|
-
return base.replace(/\.md$/, '');
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
export interface CreateSessionOpts {
|
|
103
|
-
branch?: string;
|
|
104
|
-
planFile?: string;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Create a session (INSERT OR IGNORE for idempotency).
|
|
109
|
-
*/
|
|
110
|
-
export function createSession(db: Database.Database, sessionId: string, opts?: CreateSessionOpts): void {
|
|
111
|
-
const now = new Date();
|
|
112
|
-
const taskId = autoDetectTaskId(opts?.planFile);
|
|
113
|
-
db.prepare(`
|
|
114
|
-
INSERT OR IGNORE INTO sessions (session_id, git_branch, plan_file, task_id, started_at, started_at_epoch)
|
|
115
|
-
VALUES (?, ?, ?, ?, ?, ?)
|
|
116
|
-
`).run(sessionId, opts?.branch ?? null, opts?.planFile ?? null, taskId, now.toISOString(), Math.floor(now.getTime() / 1000));
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* End a session by updating status and ended_at.
|
|
121
|
-
*/
|
|
122
|
-
export function endSession(db: Database.Database, sessionId: string, status: 'completed' | 'abandoned' = 'completed'): void {
|
|
123
|
-
const now = new Date();
|
|
124
|
-
db.prepare(`
|
|
125
|
-
UPDATE sessions SET status = ?, ended_at = ?, ended_at_epoch = ? WHERE session_id = ?
|
|
126
|
-
`).run(status, now.toISOString(), Math.floor(now.getTime() / 1000), sessionId);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
export interface AddObservationOpts {
|
|
130
|
-
filesInvolved?: string[];
|
|
131
|
-
planItem?: string;
|
|
132
|
-
crRule?: string;
|
|
133
|
-
vrType?: string;
|
|
134
|
-
evidence?: string;
|
|
135
|
-
importance?: number;
|
|
136
|
-
originalTokens?: number;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Insert an observation into the memory DB.
|
|
141
|
-
*/
|
|
142
|
-
export function addObservation(
|
|
143
|
-
db: Database.Database,
|
|
144
|
-
sessionId: string,
|
|
145
|
-
type: string,
|
|
146
|
-
title: string,
|
|
147
|
-
detail: string | null,
|
|
148
|
-
opts?: AddObservationOpts
|
|
149
|
-
): number {
|
|
150
|
-
const now = new Date();
|
|
151
|
-
const importance = opts?.importance ?? assignImportance(type, opts?.evidence?.includes('PASS') ? 'PASS' : undefined);
|
|
152
|
-
const result = db.prepare(`
|
|
153
|
-
INSERT INTO observations (session_id, type, title, detail, files_involved, plan_item, cr_rule, vr_type, evidence, importance, original_tokens, created_at, created_at_epoch)
|
|
154
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
155
|
-
`).run(
|
|
156
|
-
sessionId, type, title, detail,
|
|
157
|
-
JSON.stringify(opts?.filesInvolved ?? []),
|
|
158
|
-
opts?.planItem ?? null,
|
|
159
|
-
opts?.crRule ?? null,
|
|
160
|
-
opts?.vrType ?? null,
|
|
161
|
-
opts?.evidence ?? null,
|
|
162
|
-
importance,
|
|
163
|
-
opts?.originalTokens ?? 0,
|
|
164
|
-
now.toISOString(),
|
|
165
|
-
Math.floor(now.getTime() / 1000)
|
|
166
|
-
);
|
|
167
|
-
return Number(result.lastInsertRowid);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
export interface SessionSummary {
|
|
171
|
-
request?: string;
|
|
172
|
-
investigated?: string;
|
|
173
|
-
decisions?: string;
|
|
174
|
-
completed?: string;
|
|
175
|
-
failedAttempts?: string;
|
|
176
|
-
nextSteps?: string;
|
|
177
|
-
filesCreated?: string[];
|
|
178
|
-
filesModified?: string[];
|
|
179
|
-
verificationResults?: Record<string, string>;
|
|
180
|
-
planProgress?: Record<string, string>;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* Insert a session summary.
|
|
185
|
-
*/
|
|
186
|
-
export function addSummary(db: Database.Database, sessionId: string, summary: SessionSummary): void {
|
|
187
|
-
const now = new Date();
|
|
188
|
-
db.prepare(`
|
|
189
|
-
INSERT INTO session_summaries (session_id, request, investigated, decisions, completed, failed_attempts, next_steps, files_created, files_modified, verification_results, plan_progress, created_at, created_at_epoch)
|
|
190
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
191
|
-
`).run(
|
|
192
|
-
sessionId,
|
|
193
|
-
summary.request ?? null,
|
|
194
|
-
summary.investigated ?? null,
|
|
195
|
-
summary.decisions ?? null,
|
|
196
|
-
summary.completed ?? null,
|
|
197
|
-
summary.failedAttempts ?? null,
|
|
198
|
-
summary.nextSteps ?? null,
|
|
199
|
-
JSON.stringify(summary.filesCreated ?? []),
|
|
200
|
-
JSON.stringify(summary.filesModified ?? []),
|
|
201
|
-
JSON.stringify(summary.verificationResults ?? {}),
|
|
202
|
-
JSON.stringify(summary.planProgress ?? {}),
|
|
203
|
-
now.toISOString(),
|
|
204
|
-
Math.floor(now.getTime() / 1000)
|
|
205
|
-
);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* Insert a user prompt.
|
|
210
|
-
*/
|
|
211
|
-
export function addUserPrompt(db: Database.Database, sessionId: string, text: string, promptNumber: number): void {
|
|
212
|
-
const now = new Date();
|
|
213
|
-
db.prepare(`
|
|
214
|
-
INSERT INTO user_prompts (session_id, prompt_text, prompt_number, created_at, created_at_epoch)
|
|
215
|
-
VALUES (?, ?, ?, ?, ?)
|
|
216
|
-
`).run(sessionId, text, promptNumber, now.toISOString(), Math.floor(now.getTime() / 1000));
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
export interface SearchOpts {
|
|
220
|
-
type?: string;
|
|
221
|
-
crRule?: string;
|
|
222
|
-
dateFrom?: string;
|
|
223
|
-
limit?: number;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
/**
|
|
227
|
-
* FTS5 search on observations + user_prompts.
|
|
228
|
-
*/
|
|
229
|
-
export function searchObservations(db: Database.Database, query: string, opts?: SearchOpts): Array<{
|
|
230
|
-
id: number;
|
|
231
|
-
type: string;
|
|
232
|
-
title: string;
|
|
233
|
-
created_at: string;
|
|
234
|
-
session_id: string;
|
|
235
|
-
importance: number;
|
|
236
|
-
rank: number;
|
|
237
|
-
}> {
|
|
238
|
-
const limit = opts?.limit ?? 20;
|
|
239
|
-
let sql = `
|
|
240
|
-
SELECT o.id, o.type, o.title, o.created_at, o.session_id, o.importance,
|
|
241
|
-
rank
|
|
242
|
-
FROM observations_fts
|
|
243
|
-
JOIN observations o ON observations_fts.rowid = o.id
|
|
244
|
-
WHERE observations_fts MATCH ?
|
|
245
|
-
`;
|
|
246
|
-
const params: (string | number)[] = [query];
|
|
247
|
-
|
|
248
|
-
if (opts?.type) {
|
|
249
|
-
sql += ' AND o.type = ?';
|
|
250
|
-
params.push(opts.type);
|
|
251
|
-
}
|
|
252
|
-
if (opts?.crRule) {
|
|
253
|
-
sql += ' AND o.cr_rule = ?';
|
|
254
|
-
params.push(opts.crRule);
|
|
255
|
-
}
|
|
256
|
-
if (opts?.dateFrom) {
|
|
257
|
-
sql += ' AND o.created_at >= ?';
|
|
258
|
-
params.push(opts.dateFrom);
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
sql += ' ORDER BY rank LIMIT ?';
|
|
262
|
-
params.push(limit);
|
|
263
|
-
|
|
264
|
-
return db.prepare(sql).all(...params) as Array<{
|
|
265
|
-
id: number;
|
|
266
|
-
type: string;
|
|
267
|
-
title: string;
|
|
268
|
-
created_at: string;
|
|
269
|
-
session_id: string;
|
|
270
|
-
importance: number;
|
|
271
|
-
rank: number;
|
|
272
|
-
}>;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
/**
|
|
276
|
-
* Get recent observations, optionally filtered by session.
|
|
277
|
-
*/
|
|
278
|
-
export function getRecentObservations(db: Database.Database, limit: number = 20, sessionId?: string): Array<{
|
|
279
|
-
id: number;
|
|
280
|
-
type: string;
|
|
281
|
-
title: string;
|
|
282
|
-
detail: string | null;
|
|
283
|
-
importance: number;
|
|
284
|
-
created_at: string;
|
|
285
|
-
session_id: string;
|
|
286
|
-
}> {
|
|
287
|
-
if (sessionId) {
|
|
288
|
-
return db.prepare(`
|
|
289
|
-
SELECT id, type, title, detail, importance, created_at, session_id
|
|
290
|
-
FROM observations WHERE session_id = ?
|
|
291
|
-
ORDER BY created_at_epoch DESC LIMIT ?
|
|
292
|
-
`).all(sessionId, limit) as Array<{
|
|
293
|
-
id: number; type: string; title: string; detail: string | null;
|
|
294
|
-
importance: number; created_at: string; session_id: string;
|
|
295
|
-
}>;
|
|
296
|
-
}
|
|
297
|
-
return db.prepare(`
|
|
298
|
-
SELECT id, type, title, detail, importance, created_at, session_id
|
|
299
|
-
FROM observations
|
|
300
|
-
ORDER BY created_at_epoch DESC LIMIT ?
|
|
301
|
-
`).all(limit) as Array<{
|
|
302
|
-
id: number; type: string; title: string; detail: string | null;
|
|
303
|
-
importance: number; created_at: string; session_id: string;
|
|
304
|
-
}>;
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
/**
|
|
308
|
-
* Get recent session summaries.
|
|
309
|
-
*/
|
|
310
|
-
export function getSessionSummaries(db: Database.Database, limit: number = 10): Array<{
|
|
311
|
-
session_id: string;
|
|
312
|
-
request: string | null;
|
|
313
|
-
completed: string | null;
|
|
314
|
-
failed_attempts: string | null;
|
|
315
|
-
plan_progress: string;
|
|
316
|
-
created_at: string;
|
|
317
|
-
}> {
|
|
318
|
-
return db.prepare(`
|
|
319
|
-
SELECT session_id, request, completed, failed_attempts, plan_progress, created_at
|
|
320
|
-
FROM session_summaries
|
|
321
|
-
ORDER BY created_at_epoch DESC LIMIT ?
|
|
322
|
-
`).all(limit) as Array<{
|
|
323
|
-
session_id: string; request: string | null; completed: string | null;
|
|
324
|
-
failed_attempts: string | null; plan_progress: string; created_at: string;
|
|
325
|
-
}>;
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
/**
|
|
329
|
-
* Get complete timeline for a session.
|
|
330
|
-
*/
|
|
331
|
-
export function getSessionTimeline(db: Database.Database, sessionId: string): {
|
|
332
|
-
session: Record<string, unknown> | null;
|
|
333
|
-
observations: Array<Record<string, unknown>>;
|
|
334
|
-
summary: Record<string, unknown> | null;
|
|
335
|
-
prompts: Array<Record<string, unknown>>;
|
|
336
|
-
} {
|
|
337
|
-
const session = db.prepare('SELECT * FROM sessions WHERE session_id = ?').get(sessionId) as Record<string, unknown> | undefined;
|
|
338
|
-
const observations = db.prepare('SELECT * FROM observations WHERE session_id = ? ORDER BY created_at_epoch ASC').all(sessionId) as Array<Record<string, unknown>>;
|
|
339
|
-
const summary = db.prepare('SELECT * FROM session_summaries WHERE session_id = ? ORDER BY created_at_epoch DESC LIMIT 1').get(sessionId) as Record<string, unknown> | undefined;
|
|
340
|
-
const prompts = db.prepare('SELECT * FROM user_prompts WHERE session_id = ? ORDER BY prompt_number ASC').all(sessionId) as Array<Record<string, unknown>>;
|
|
341
|
-
|
|
342
|
-
return {
|
|
343
|
-
session: session ?? null,
|
|
344
|
-
observations,
|
|
345
|
-
summary: summary ?? null,
|
|
346
|
-
prompts,
|
|
347
|
-
};
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
/**
|
|
351
|
-
* Get failed attempt observations.
|
|
352
|
-
*/
|
|
353
|
-
export function getFailedAttempts(db: Database.Database, query?: string, limit: number = 20): Array<{
|
|
354
|
-
id: number;
|
|
355
|
-
title: string;
|
|
356
|
-
detail: string | null;
|
|
357
|
-
session_id: string;
|
|
358
|
-
recurrence_count: number;
|
|
359
|
-
created_at: string;
|
|
360
|
-
}> {
|
|
361
|
-
if (query) {
|
|
362
|
-
return db.prepare(`
|
|
363
|
-
SELECT o.id, o.title, o.detail, o.session_id, o.recurrence_count, o.created_at
|
|
364
|
-
FROM observations_fts
|
|
365
|
-
JOIN observations o ON observations_fts.rowid = o.id
|
|
366
|
-
WHERE observations_fts MATCH ? AND o.type = 'failed_attempt'
|
|
367
|
-
ORDER BY o.recurrence_count DESC, rank LIMIT ?
|
|
368
|
-
`).all(query, limit) as Array<{
|
|
369
|
-
id: number; title: string; detail: string | null; session_id: string;
|
|
370
|
-
recurrence_count: number; created_at: string;
|
|
371
|
-
}>;
|
|
372
|
-
}
|
|
373
|
-
return db.prepare(`
|
|
374
|
-
SELECT id, title, detail, session_id, recurrence_count, created_at
|
|
375
|
-
FROM observations WHERE type = 'failed_attempt'
|
|
376
|
-
ORDER BY recurrence_count DESC, created_at_epoch DESC LIMIT ?
|
|
377
|
-
`).all(limit) as Array<{
|
|
378
|
-
id: number; title: string; detail: string | null; session_id: string;
|
|
379
|
-
recurrence_count: number; created_at: string;
|
|
380
|
-
}>;
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
/**
|
|
384
|
-
* Search decision observations.
|
|
385
|
-
*/
|
|
386
|
-
export function getDecisionsAbout(db: Database.Database, query: string, limit: number = 20): Array<{
|
|
387
|
-
id: number;
|
|
388
|
-
title: string;
|
|
389
|
-
detail: string | null;
|
|
390
|
-
session_id: string;
|
|
391
|
-
created_at: string;
|
|
392
|
-
}> {
|
|
393
|
-
return db.prepare(`
|
|
394
|
-
SELECT o.id, o.title, o.detail, o.session_id, o.created_at
|
|
395
|
-
FROM observations_fts
|
|
396
|
-
JOIN observations o ON observations_fts.rowid = o.id
|
|
397
|
-
WHERE observations_fts MATCH ? AND o.type = 'decision'
|
|
398
|
-
ORDER BY rank LIMIT ?
|
|
399
|
-
`).all(query, limit) as Array<{
|
|
400
|
-
id: number; title: string; detail: string | null; session_id: string;
|
|
401
|
-
created_at: string;
|
|
402
|
-
}>;
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
/**
|
|
406
|
-
* Delete observations older than retention period.
|
|
407
|
-
*/
|
|
408
|
-
export function pruneOldObservations(db: Database.Database, retentionDays: number = 90): number {
|
|
409
|
-
const cutoffEpoch = Math.floor(Date.now() / 1000) - (retentionDays * 86400);
|
|
410
|
-
const result = db.prepare('DELETE FROM observations WHERE created_at_epoch < ?').run(cutoffEpoch);
|
|
411
|
-
return result.changes;
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
/**
|
|
415
|
-
* Deduplicate failed attempts across sessions.
|
|
416
|
-
* If the same failure title exists, increment recurrence_count instead of creating a duplicate.
|
|
417
|
-
*/
|
|
418
|
-
export function deduplicateFailedAttempt(
|
|
419
|
-
db: Database.Database,
|
|
420
|
-
sessionId: string,
|
|
421
|
-
title: string,
|
|
422
|
-
detail: string | null,
|
|
423
|
-
opts?: AddObservationOpts
|
|
424
|
-
): number {
|
|
425
|
-
// Check if a similar failed_attempt already exists (across all sessions)
|
|
426
|
-
const existing = db.prepare(`
|
|
427
|
-
SELECT id, recurrence_count FROM observations
|
|
428
|
-
WHERE type = 'failed_attempt' AND title = ?
|
|
429
|
-
ORDER BY created_at_epoch DESC LIMIT 1
|
|
430
|
-
`).get(title) as { id: number; recurrence_count: number } | undefined;
|
|
431
|
-
|
|
432
|
-
if (existing) {
|
|
433
|
-
// Increment recurrence count and update detail if newer
|
|
434
|
-
db.prepare('UPDATE observations SET recurrence_count = recurrence_count + 1, detail = COALESCE(?, detail) WHERE id = ?')
|
|
435
|
-
.run(detail, existing.id);
|
|
436
|
-
return existing.id;
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
// New failed attempt
|
|
440
|
-
return addObservation(db, sessionId, 'failed_attempt', title, detail, {
|
|
441
|
-
...opts,
|
|
442
|
-
importance: 5,
|
|
443
|
-
});
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
/**
|
|
447
|
-
* Get all sessions linked to a task/plan.
|
|
448
|
-
*/
|
|
449
|
-
export function getSessionsByTask(db: Database.Database, taskId: string): Array<{
|
|
450
|
-
session_id: string;
|
|
451
|
-
status: string;
|
|
452
|
-
started_at: string;
|
|
453
|
-
ended_at: string | null;
|
|
454
|
-
plan_phase: string | null;
|
|
455
|
-
}> {
|
|
456
|
-
return db.prepare(`
|
|
457
|
-
SELECT session_id, status, started_at, ended_at, plan_phase
|
|
458
|
-
FROM sessions WHERE task_id = ?
|
|
459
|
-
ORDER BY started_at_epoch DESC
|
|
460
|
-
`).all(taskId) as Array<{
|
|
461
|
-
session_id: string; status: string; started_at: string;
|
|
462
|
-
ended_at: string | null; plan_phase: string | null;
|
|
463
|
-
}>;
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
/**
|
|
467
|
-
* Aggregate plan_progress across all sessions for a task.
|
|
468
|
-
*/
|
|
469
|
-
export function getCrossTaskProgress(db: Database.Database, taskId: string): Record<string, string> {
|
|
470
|
-
const sessions = db.prepare(`
|
|
471
|
-
SELECT session_id FROM sessions WHERE task_id = ?
|
|
472
|
-
`).all(taskId) as Array<{ session_id: string }>;
|
|
473
|
-
|
|
474
|
-
const merged: Record<string, string> = {};
|
|
475
|
-
for (const session of sessions) {
|
|
476
|
-
const summaries = db.prepare(`
|
|
477
|
-
SELECT plan_progress FROM session_summaries WHERE session_id = ?
|
|
478
|
-
`).all(session.session_id) as Array<{ plan_progress: string }>;
|
|
479
|
-
|
|
480
|
-
for (const summary of summaries) {
|
|
481
|
-
try {
|
|
482
|
-
const progress = JSON.parse(summary.plan_progress) as Record<string, string>;
|
|
483
|
-
for (const [key, value] of Object.entries(progress)) {
|
|
484
|
-
// Later status wins (complete > in_progress > pending)
|
|
485
|
-
if (!merged[key] || value === 'complete' || (value === 'in_progress' && merged[key] === 'pending')) {
|
|
486
|
-
merged[key] = value;
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
} catch (_e) {
|
|
490
|
-
// Skip invalid JSON
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
return merged;
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
/**
|
|
499
|
-
* Set task_id on a session for multi-session task linking.
|
|
500
|
-
*/
|
|
501
|
-
export function linkSessionToTask(db: Database.Database, sessionId: string, taskId: string): void {
|
|
502
|
-
db.prepare('UPDATE sessions SET task_id = ? WHERE session_id = ?').run(taskId, sessionId);
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
// ============================================================
|
|
506
|
-
// Observability Functions (P2-002, P2-003, P4-001)
|
|
507
|
-
// ============================================================
|
|
508
|
-
|
|
509
|
-
/**
|
|
510
|
-
* Insert a conversation turn into the observability table.
|
|
511
|
-
* Returns the new row ID.
|
|
512
|
-
*/
|
|
513
|
-
export function addConversationTurn(
|
|
514
|
-
db: Database.Database,
|
|
515
|
-
sessionId: string,
|
|
516
|
-
turnNumber: number,
|
|
517
|
-
userPrompt: string,
|
|
518
|
-
assistantResponse: string | null,
|
|
519
|
-
toolCallsJson: string | null,
|
|
520
|
-
toolCallCount: number,
|
|
521
|
-
promptTokens: number,
|
|
522
|
-
responseTokens: number
|
|
523
|
-
): number {
|
|
524
|
-
const result = db.prepare(`
|
|
525
|
-
INSERT INTO conversation_turns (session_id, turn_number, user_prompt, assistant_response, tool_calls_json, tool_call_count, prompt_tokens, response_tokens)
|
|
526
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
527
|
-
`).run(
|
|
528
|
-
sessionId, turnNumber, userPrompt,
|
|
529
|
-
assistantResponse ? assistantResponse.slice(0, 10000) : null,
|
|
530
|
-
toolCallsJson, toolCallCount, promptTokens, responseTokens
|
|
531
|
-
);
|
|
532
|
-
return Number(result.lastInsertRowid);
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
/**
|
|
536
|
-
* Insert a tool call detail record.
|
|
537
|
-
*/
|
|
538
|
-
export function addToolCallDetail(
|
|
539
|
-
db: Database.Database,
|
|
540
|
-
sessionId: string,
|
|
541
|
-
turnNumber: number,
|
|
542
|
-
toolName: string,
|
|
543
|
-
inputSummary: string | null,
|
|
544
|
-
inputSize: number,
|
|
545
|
-
outputSize: number,
|
|
546
|
-
success: boolean,
|
|
547
|
-
filesInvolved?: string[]
|
|
548
|
-
): void {
|
|
549
|
-
db.prepare(`
|
|
550
|
-
INSERT INTO tool_call_details (session_id, turn_number, tool_name, tool_input_summary, tool_input_size, tool_output_size, tool_success, files_involved)
|
|
551
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
552
|
-
`).run(
|
|
553
|
-
sessionId, turnNumber, toolName,
|
|
554
|
-
inputSummary ? inputSummary.slice(0, 500) : null,
|
|
555
|
-
inputSize, outputSize, success ? 1 : 0,
|
|
556
|
-
filesInvolved ? JSON.stringify(filesInvolved) : null
|
|
557
|
-
);
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
/**
|
|
561
|
-
* Get the last processed line number for incremental transcript parsing.
|
|
562
|
-
*/
|
|
563
|
-
export function getLastProcessedLine(db: Database.Database, sessionId: string): number {
|
|
564
|
-
const row = db.prepare('SELECT value FROM memory_meta WHERE key = ?').get(`last_processed_line:${sessionId}`) as { value: string } | undefined;
|
|
565
|
-
return row ? parseInt(row.value, 10) : 0;
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
/**
|
|
569
|
-
* Set the last processed line number for incremental transcript parsing.
|
|
570
|
-
*/
|
|
571
|
-
export function setLastProcessedLine(db: Database.Database, sessionId: string, lineNumber: number): void {
|
|
572
|
-
db.prepare('INSERT OR REPLACE INTO memory_meta (key, value) VALUES (?, ?)').run(`last_processed_line:${sessionId}`, String(lineNumber));
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
/**
|
|
576
|
-
* Delete conversation turns and tool call details older than retention period.
|
|
577
|
-
*/
|
|
578
|
-
export function pruneOldConversationTurns(db: Database.Database, retentionDays: number = 90): { turnsDeleted: number; detailsDeleted: number } {
|
|
579
|
-
const cutoffEpoch = Math.floor(Date.now() / 1000) - (retentionDays * 86400);
|
|
580
|
-
const turnsResult = db.prepare('DELETE FROM conversation_turns WHERE created_at_epoch < ?').run(cutoffEpoch);
|
|
581
|
-
const detailsResult = db.prepare('DELETE FROM tool_call_details WHERE created_at_epoch < ?').run(cutoffEpoch);
|
|
582
|
-
return { turnsDeleted: turnsResult.changes, detailsDeleted: detailsResult.changes };
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
/**
|
|
586
|
-
* Get conversation turns for a session (for replay).
|
|
587
|
-
*/
|
|
588
|
-
export function getConversationTurns(db: Database.Database, sessionId: string, opts?: {
|
|
589
|
-
turnFrom?: number;
|
|
590
|
-
turnTo?: number;
|
|
591
|
-
includeToolCalls?: boolean;
|
|
592
|
-
}): Array<{
|
|
593
|
-
id: number;
|
|
594
|
-
turn_number: number;
|
|
595
|
-
user_prompt: string;
|
|
596
|
-
assistant_response: string | null;
|
|
597
|
-
tool_calls_json: string | null;
|
|
598
|
-
tool_call_count: number;
|
|
599
|
-
prompt_tokens: number | null;
|
|
600
|
-
response_tokens: number | null;
|
|
601
|
-
created_at: string;
|
|
602
|
-
}> {
|
|
603
|
-
let sql = 'SELECT id, turn_number, user_prompt, assistant_response, tool_calls_json, tool_call_count, prompt_tokens, response_tokens, created_at FROM conversation_turns WHERE session_id = ?';
|
|
604
|
-
const params: (string | number)[] = [sessionId];
|
|
605
|
-
|
|
606
|
-
if (opts?.turnFrom !== undefined) {
|
|
607
|
-
sql += ' AND turn_number >= ?';
|
|
608
|
-
params.push(opts.turnFrom);
|
|
609
|
-
}
|
|
610
|
-
if (opts?.turnTo !== undefined) {
|
|
611
|
-
sql += ' AND turn_number <= ?';
|
|
612
|
-
params.push(opts.turnTo);
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
sql += ' ORDER BY turn_number ASC';
|
|
616
|
-
|
|
617
|
-
return db.prepare(sql).all(...params) as Array<{
|
|
618
|
-
id: number; turn_number: number; user_prompt: string;
|
|
619
|
-
assistant_response: string | null; tool_calls_json: string | null;
|
|
620
|
-
tool_call_count: number; prompt_tokens: number | null;
|
|
621
|
-
response_tokens: number | null; created_at: string;
|
|
622
|
-
}>;
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
/**
|
|
626
|
-
* Search conversation turns using FTS5.
|
|
627
|
-
*/
|
|
628
|
-
export function searchConversationTurns(db: Database.Database, query: string, opts?: {
|
|
629
|
-
sessionId?: string;
|
|
630
|
-
dateFrom?: string;
|
|
631
|
-
dateTo?: string;
|
|
632
|
-
minToolCalls?: number;
|
|
633
|
-
limit?: number;
|
|
634
|
-
}): Array<{
|
|
635
|
-
id: number;
|
|
636
|
-
session_id: string;
|
|
637
|
-
turn_number: number;
|
|
638
|
-
user_prompt: string;
|
|
639
|
-
tool_call_count: number;
|
|
640
|
-
response_tokens: number | null;
|
|
641
|
-
created_at: string;
|
|
642
|
-
rank: number;
|
|
643
|
-
}> {
|
|
644
|
-
const limit = opts?.limit ?? 20;
|
|
645
|
-
let sql = `
|
|
646
|
-
SELECT ct.id, ct.session_id, ct.turn_number, ct.user_prompt, ct.tool_call_count, ct.response_tokens, ct.created_at, rank
|
|
647
|
-
FROM conversation_turns_fts
|
|
648
|
-
JOIN conversation_turns ct ON conversation_turns_fts.rowid = ct.id
|
|
649
|
-
WHERE conversation_turns_fts MATCH ?
|
|
650
|
-
`;
|
|
651
|
-
const params: (string | number)[] = [query];
|
|
652
|
-
|
|
653
|
-
if (opts?.sessionId) {
|
|
654
|
-
sql += ' AND ct.session_id = ?';
|
|
655
|
-
params.push(opts.sessionId);
|
|
656
|
-
}
|
|
657
|
-
if (opts?.dateFrom) {
|
|
658
|
-
sql += ' AND ct.created_at >= ?';
|
|
659
|
-
params.push(opts.dateFrom);
|
|
660
|
-
}
|
|
661
|
-
if (opts?.dateTo) {
|
|
662
|
-
sql += ' AND ct.created_at <= ?';
|
|
663
|
-
params.push(opts.dateTo);
|
|
664
|
-
}
|
|
665
|
-
if (opts?.minToolCalls !== undefined) {
|
|
666
|
-
sql += ' AND ct.tool_call_count >= ?';
|
|
667
|
-
params.push(opts.minToolCalls);
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
sql += ' ORDER BY rank LIMIT ?';
|
|
671
|
-
params.push(limit);
|
|
672
|
-
|
|
673
|
-
return db.prepare(sql).all(...params) as Array<{
|
|
674
|
-
id: number; session_id: string; turn_number: number;
|
|
675
|
-
user_prompt: string; tool_call_count: number;
|
|
676
|
-
response_tokens: number | null; created_at: string; rank: number;
|
|
677
|
-
}>;
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
/**
|
|
681
|
-
* Get tool usage patterns (aggregated stats).
|
|
682
|
-
*/
|
|
683
|
-
export function getToolPatterns(db: Database.Database, opts?: {
|
|
684
|
-
sessionId?: string;
|
|
685
|
-
toolName?: string;
|
|
686
|
-
dateFrom?: string;
|
|
687
|
-
groupBy?: 'tool' | 'session' | 'day';
|
|
688
|
-
}): Array<Record<string, unknown>> {
|
|
689
|
-
const groupBy = opts?.groupBy ?? 'tool';
|
|
690
|
-
const params: (string | number)[] = [];
|
|
691
|
-
let whereClause = '';
|
|
692
|
-
const conditions: string[] = [];
|
|
693
|
-
|
|
694
|
-
if (opts?.sessionId) {
|
|
695
|
-
conditions.push('session_id = ?');
|
|
696
|
-
params.push(opts.sessionId);
|
|
697
|
-
}
|
|
698
|
-
if (opts?.toolName) {
|
|
699
|
-
conditions.push('tool_name = ?');
|
|
700
|
-
params.push(opts.toolName);
|
|
701
|
-
}
|
|
702
|
-
if (opts?.dateFrom) {
|
|
703
|
-
conditions.push('created_at >= ?');
|
|
704
|
-
params.push(opts.dateFrom);
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
if (conditions.length > 0) {
|
|
708
|
-
whereClause = 'WHERE ' + conditions.join(' AND ');
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
let sql: string;
|
|
712
|
-
switch (groupBy) {
|
|
713
|
-
case 'session':
|
|
714
|
-
sql = `SELECT session_id, COUNT(*) as call_count, COUNT(DISTINCT tool_name) as unique_tools,
|
|
715
|
-
SUM(CASE WHEN tool_success = 1 THEN 1 ELSE 0 END) as successes,
|
|
716
|
-
SUM(CASE WHEN tool_success = 0 THEN 1 ELSE 0 END) as failures,
|
|
717
|
-
AVG(tool_output_size) as avg_output_size
|
|
718
|
-
FROM tool_call_details ${whereClause}
|
|
719
|
-
GROUP BY session_id ORDER BY call_count DESC`;
|
|
720
|
-
break;
|
|
721
|
-
case 'day':
|
|
722
|
-
sql = `SELECT date(created_at) as day, COUNT(*) as call_count, COUNT(DISTINCT tool_name) as unique_tools,
|
|
723
|
-
SUM(CASE WHEN tool_success = 1 THEN 1 ELSE 0 END) as successes
|
|
724
|
-
FROM tool_call_details ${whereClause}
|
|
725
|
-
GROUP BY date(created_at) ORDER BY day DESC`;
|
|
726
|
-
break;
|
|
727
|
-
default: // 'tool'
|
|
728
|
-
sql = `SELECT tool_name, COUNT(*) as call_count,
|
|
729
|
-
SUM(CASE WHEN tool_success = 1 THEN 1 ELSE 0 END) as successes,
|
|
730
|
-
SUM(CASE WHEN tool_success = 0 THEN 1 ELSE 0 END) as failures,
|
|
731
|
-
AVG(tool_output_size) as avg_output_size,
|
|
732
|
-
AVG(tool_input_size) as avg_input_size
|
|
733
|
-
FROM tool_call_details ${whereClause}
|
|
734
|
-
GROUP BY tool_name ORDER BY call_count DESC`;
|
|
735
|
-
break;
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
return db.prepare(sql).all(...params) as Array<Record<string, unknown>>;
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
/**
|
|
742
|
-
* Get session stats for observability.
|
|
743
|
-
*/
|
|
744
|
-
export function getSessionStats(db: Database.Database, opts?: {
|
|
745
|
-
sessionId?: string;
|
|
746
|
-
limit?: number;
|
|
747
|
-
}): Array<Record<string, unknown>> {
|
|
748
|
-
if (opts?.sessionId) {
|
|
749
|
-
// Single session stats
|
|
750
|
-
const turns = db.prepare('SELECT COUNT(*) as turn_count, SUM(tool_call_count) as total_tool_calls, SUM(prompt_tokens) as total_prompt_tokens, SUM(response_tokens) as total_response_tokens FROM conversation_turns WHERE session_id = ?').get(opts.sessionId) as Record<string, unknown>;
|
|
751
|
-
const toolBreakdown = db.prepare('SELECT tool_name, COUNT(*) as count FROM tool_call_details WHERE session_id = ? GROUP BY tool_name ORDER BY count DESC').all(opts.sessionId) as Array<Record<string, unknown>>;
|
|
752
|
-
const session = db.prepare('SELECT * FROM sessions WHERE session_id = ?').get(opts.sessionId) as Record<string, unknown> | undefined;
|
|
753
|
-
|
|
754
|
-
return [{
|
|
755
|
-
session_id: opts.sessionId,
|
|
756
|
-
status: session?.status ?? 'unknown',
|
|
757
|
-
started_at: session?.started_at ?? null,
|
|
758
|
-
ended_at: session?.ended_at ?? null,
|
|
759
|
-
...turns,
|
|
760
|
-
tool_breakdown: toolBreakdown,
|
|
761
|
-
}];
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
const limit = opts?.limit ?? 10;
|
|
765
|
-
return db.prepare(`
|
|
766
|
-
SELECT s.session_id, s.status, s.started_at, s.ended_at,
|
|
767
|
-
COUNT(ct.id) as turn_count,
|
|
768
|
-
COALESCE(SUM(ct.tool_call_count), 0) as total_tool_calls,
|
|
769
|
-
COALESCE(SUM(ct.prompt_tokens), 0) as total_prompt_tokens,
|
|
770
|
-
COALESCE(SUM(ct.response_tokens), 0) as total_response_tokens
|
|
771
|
-
FROM sessions s
|
|
772
|
-
LEFT JOIN conversation_turns ct ON s.session_id = ct.session_id
|
|
773
|
-
GROUP BY s.session_id
|
|
774
|
-
ORDER BY s.started_at_epoch DESC
|
|
775
|
-
LIMIT ?
|
|
776
|
-
`).all(limit) as Array<Record<string, unknown>>;
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
/**
|
|
780
|
-
* Get database size information for observability monitoring.
|
|
781
|
-
*/
|
|
782
|
-
export function getObservabilityDbSize(db: Database.Database): {
|
|
783
|
-
conversation_turns_count: number;
|
|
784
|
-
tool_call_details_count: number;
|
|
785
|
-
observations_count: number;
|
|
786
|
-
db_page_count: number;
|
|
787
|
-
db_page_size: number;
|
|
788
|
-
estimated_size_mb: number;
|
|
789
|
-
} {
|
|
790
|
-
const turnsCount = (db.prepare('SELECT COUNT(*) as c FROM conversation_turns').get() as { c: number }).c;
|
|
791
|
-
const detailsCount = (db.prepare('SELECT COUNT(*) as c FROM tool_call_details').get() as { c: number }).c;
|
|
792
|
-
const obsCount = (db.prepare('SELECT COUNT(*) as c FROM observations').get() as { c: number }).c;
|
|
793
|
-
const pageCount = (db.pragma('page_count') as Array<{ page_count: number }>)[0]?.page_count ?? 0;
|
|
794
|
-
const pageSize = (db.pragma('page_size') as Array<{ page_size: number }>)[0]?.page_size ?? 4096;
|
|
795
|
-
|
|
796
|
-
return {
|
|
797
|
-
conversation_turns_count: turnsCount,
|
|
798
|
-
tool_call_details_count: detailsCount,
|
|
799
|
-
observations_count: obsCount,
|
|
800
|
-
db_page_count: pageCount,
|
|
801
|
-
db_page_size: pageSize,
|
|
802
|
-
estimated_size_mb: Math.round((pageCount * pageSize) / (1024 * 1024) * 100) / 100,
|
|
803
|
-
};
|
|
804
|
-
}
|