kiro-memory 1.9.0 → 3.0.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 (34) hide show
  1. package/README.md +5 -1
  2. package/package.json +5 -5
  3. package/plugin/dist/cli/contextkit.js +2611 -345
  4. package/plugin/dist/hooks/agentSpawn.js +853 -223
  5. package/plugin/dist/hooks/kiro-hooks.js +841 -211
  6. package/plugin/dist/hooks/postToolUse.js +853 -222
  7. package/plugin/dist/hooks/stop.js +850 -220
  8. package/plugin/dist/hooks/userPromptSubmit.js +848 -216
  9. package/plugin/dist/index.js +843 -340
  10. package/plugin/dist/plugins/github/github-client.js +152 -0
  11. package/plugin/dist/plugins/github/index.js +412 -0
  12. package/plugin/dist/plugins/github/issue-parser.js +54 -0
  13. package/plugin/dist/plugins/slack/formatter.js +90 -0
  14. package/plugin/dist/plugins/slack/index.js +215 -0
  15. package/plugin/dist/sdk/index.js +841 -215
  16. package/plugin/dist/servers/mcp-server.js +4461 -397
  17. package/plugin/dist/services/search/EmbeddingService.js +146 -37
  18. package/plugin/dist/services/search/HybridSearch.js +564 -116
  19. package/plugin/dist/services/search/VectorSearch.js +187 -60
  20. package/plugin/dist/services/search/index.js +565 -254
  21. package/plugin/dist/services/sqlite/Backup.js +416 -0
  22. package/plugin/dist/services/sqlite/Database.js +126 -153
  23. package/plugin/dist/services/sqlite/ImportExport.js +452 -0
  24. package/plugin/dist/services/sqlite/Observations.js +314 -19
  25. package/plugin/dist/services/sqlite/Prompts.js +1 -1
  26. package/plugin/dist/services/sqlite/Search.js +41 -29
  27. package/plugin/dist/services/sqlite/Summaries.js +4 -4
  28. package/plugin/dist/services/sqlite/index.js +1428 -208
  29. package/plugin/dist/viewer.css +1 -0
  30. package/plugin/dist/viewer.html +2 -179
  31. package/plugin/dist/viewer.js +23 -24942
  32. package/plugin/dist/viewer.js.map +7 -0
  33. package/plugin/dist/worker-service.js +427 -5569
  34. package/plugin/dist/worker-service.js.map +7 -0
@@ -1,5 +1,277 @@
1
1
  import { createRequire } from 'module';const require = createRequire(import.meta.url);
2
2
 
3
+ // src/utils/secrets.ts
4
+ var SECRET_PATTERNS = [
5
+ // AWS Access Keys (AKIA, ABIA, ACCA, ASIA prefixes + 16 alphanumeric chars)
6
+ { name: "aws-key", pattern: /(?:AKIA|ABIA|ACCA|ASIA)[A-Z0-9]{16}/g },
7
+ // JWT tokens (three base64url segments separated by dots)
8
+ { name: "jwt", pattern: /eyJ[a-zA-Z0-9_-]{10,}\.eyJ[a-zA-Z0-9_-]{10,}\.[a-zA-Z0-9_-]{10,}/g },
9
+ // Generic API keys in key=value or key: value assignments
10
+ { name: "api-key", pattern: /(?:api[_-]?key|apikey|api[_-]?secret)\s*[:=]\s*['"]?([a-zA-Z0-9_\-]{20,})['"]?/gi },
11
+ // Password/secret/token in variable assignments
12
+ { name: "credential", pattern: /(?:password|passwd|pwd|secret|token|auth[_-]?token|access[_-]?token|bearer)\s*[:=]\s*['"]?([^\s'"]{8,})['"]?/gi },
13
+ // Credentials embedded in URLs (user:pass@host)
14
+ { name: "url-credential", pattern: /(?:https?:\/\/)([^:]+):([^@]+)@/g },
15
+ // PEM-encoded private keys (RSA, EC, DSA, OpenSSH)
16
+ { name: "private-key", pattern: /-----BEGIN (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----/g },
17
+ // GitHub personal access tokens (ghp_, gho_, ghu_, ghs_, ghr_ prefixes)
18
+ { name: "github-token", pattern: /gh[pousr]_[a-zA-Z0-9]{36,}/g },
19
+ // Slack bot/user/app tokens
20
+ { name: "slack-token", pattern: /xox[bpoas]-[a-zA-Z0-9-]{10,}/g },
21
+ // HTTP Authorization Bearer header values
22
+ { name: "bearer-header", pattern: /\bBearer\s+([a-zA-Z0-9_\-\.]{20,})/g },
23
+ // Generic hex secrets (32+ hex chars after a key/secret/token/password label)
24
+ { name: "hex-secret", pattern: /(?:key|secret|token|password)\s*[:=]\s*['"]?([0-9a-f]{32,})['"]?/gi }
25
+ ];
26
+ function redactSecrets(text) {
27
+ if (!text) return text;
28
+ let redacted = text;
29
+ for (const { pattern } of SECRET_PATTERNS) {
30
+ pattern.lastIndex = 0;
31
+ redacted = redacted.replace(pattern, (match) => {
32
+ const prefix = match.substring(0, Math.min(4, match.length));
33
+ return `${prefix}***REDACTED***`;
34
+ });
35
+ }
36
+ return redacted;
37
+ }
38
+
39
+ // src/utils/categorizer.ts
40
+ var CATEGORY_RULES = [
41
+ {
42
+ category: "security",
43
+ keywords: [
44
+ "security",
45
+ "vulnerability",
46
+ "cve",
47
+ "xss",
48
+ "csrf",
49
+ "injection",
50
+ "sanitize",
51
+ "escape",
52
+ "auth",
53
+ "authentication",
54
+ "authorization",
55
+ "permission",
56
+ "helmet",
57
+ "cors",
58
+ "rate-limit",
59
+ "token",
60
+ "encrypt",
61
+ "decrypt",
62
+ "secret",
63
+ "redact",
64
+ "owasp"
65
+ ],
66
+ filePatterns: [/security/i, /auth/i, /secrets?\.ts/i],
67
+ weight: 10
68
+ },
69
+ {
70
+ category: "testing",
71
+ keywords: [
72
+ "test",
73
+ "spec",
74
+ "expect",
75
+ "assert",
76
+ "mock",
77
+ "stub",
78
+ "fixture",
79
+ "coverage",
80
+ "jest",
81
+ "vitest",
82
+ "bun test",
83
+ "unit test",
84
+ "integration test",
85
+ "e2e"
86
+ ],
87
+ types: ["test"],
88
+ filePatterns: [/\.test\./i, /\.spec\./i, /tests?\//i, /__tests__/i],
89
+ weight: 8
90
+ },
91
+ {
92
+ category: "debugging",
93
+ keywords: [
94
+ "debug",
95
+ "fix",
96
+ "bug",
97
+ "error",
98
+ "crash",
99
+ "stacktrace",
100
+ "stack trace",
101
+ "exception",
102
+ "breakpoint",
103
+ "investigate",
104
+ "root cause",
105
+ "troubleshoot",
106
+ "diagnose",
107
+ "bisect",
108
+ "regression"
109
+ ],
110
+ types: ["bugfix"],
111
+ weight: 8
112
+ },
113
+ {
114
+ category: "architecture",
115
+ keywords: [
116
+ "architect",
117
+ "design",
118
+ "pattern",
119
+ "modular",
120
+ "migration",
121
+ "schema",
122
+ "database",
123
+ "api design",
124
+ "abstract",
125
+ "dependency injection",
126
+ "singleton",
127
+ "factory",
128
+ "observer",
129
+ "middleware",
130
+ "pipeline",
131
+ "microservice",
132
+ "monolith"
133
+ ],
134
+ types: ["decision", "constraint"],
135
+ weight: 7
136
+ },
137
+ {
138
+ category: "refactoring",
139
+ keywords: [
140
+ "refactor",
141
+ "rename",
142
+ "extract",
143
+ "inline",
144
+ "move",
145
+ "split",
146
+ "merge",
147
+ "simplify",
148
+ "cleanup",
149
+ "clean up",
150
+ "dead code",
151
+ "consolidate",
152
+ "reorganize",
153
+ "restructure",
154
+ "decouple"
155
+ ],
156
+ weight: 6
157
+ },
158
+ {
159
+ category: "config",
160
+ keywords: [
161
+ "config",
162
+ "configuration",
163
+ "env",
164
+ "environment",
165
+ "dotenv",
166
+ ".env",
167
+ "settings",
168
+ "tsconfig",
169
+ "eslint",
170
+ "prettier",
171
+ "webpack",
172
+ "vite",
173
+ "esbuild",
174
+ "docker",
175
+ "ci/cd",
176
+ "github actions",
177
+ "deploy",
178
+ "build",
179
+ "bundle",
180
+ "package.json"
181
+ ],
182
+ filePatterns: [
183
+ /\.config\./i,
184
+ /\.env/i,
185
+ /tsconfig/i,
186
+ /\.ya?ml/i,
187
+ /Dockerfile/i,
188
+ /docker-compose/i
189
+ ],
190
+ weight: 5
191
+ },
192
+ {
193
+ category: "docs",
194
+ keywords: [
195
+ "document",
196
+ "readme",
197
+ "changelog",
198
+ "jsdoc",
199
+ "comment",
200
+ "explain",
201
+ "guide",
202
+ "tutorial",
203
+ "api doc",
204
+ "openapi",
205
+ "swagger"
206
+ ],
207
+ types: ["docs"],
208
+ filePatterns: [/\.md$/i, /docs?\//i, /readme/i, /changelog/i],
209
+ weight: 5
210
+ },
211
+ {
212
+ category: "feature-dev",
213
+ keywords: [
214
+ "feature",
215
+ "implement",
216
+ "add",
217
+ "create",
218
+ "new",
219
+ "endpoint",
220
+ "component",
221
+ "module",
222
+ "service",
223
+ "handler",
224
+ "route",
225
+ "hook",
226
+ "plugin",
227
+ "integration"
228
+ ],
229
+ types: ["feature", "file-write"],
230
+ weight: 3
231
+ // lowest — generic catch-all for development
232
+ }
233
+ ];
234
+ function categorize(input) {
235
+ const scores = /* @__PURE__ */ new Map();
236
+ const searchText = [
237
+ input.title,
238
+ input.text || "",
239
+ input.narrative || "",
240
+ input.concepts || ""
241
+ ].join(" ").toLowerCase();
242
+ const allFiles = [input.filesModified || "", input.filesRead || ""].join(",");
243
+ for (const rule of CATEGORY_RULES) {
244
+ let score = 0;
245
+ for (const kw of rule.keywords) {
246
+ if (searchText.includes(kw.toLowerCase())) {
247
+ score += rule.weight;
248
+ }
249
+ }
250
+ if (rule.types && rule.types.includes(input.type)) {
251
+ score += rule.weight * 2;
252
+ }
253
+ if (rule.filePatterns && allFiles) {
254
+ for (const pattern of rule.filePatterns) {
255
+ if (pattern.test(allFiles)) {
256
+ score += rule.weight;
257
+ }
258
+ }
259
+ }
260
+ if (score > 0) {
261
+ scores.set(rule.category, (scores.get(rule.category) || 0) + score);
262
+ }
263
+ }
264
+ let bestCategory = "general";
265
+ let bestScore = 0;
266
+ for (const [category, score] of scores) {
267
+ if (score > bestScore) {
268
+ bestScore = score;
269
+ bestCategory = category;
270
+ }
271
+ }
272
+ return bestCategory;
273
+ }
274
+
3
275
  // src/services/sqlite/Observations.ts
4
276
  function escapeLikePattern(input) {
5
277
  return input.replace(/[%_\\]/g, "\\$&");
@@ -14,11 +286,23 @@ function isDuplicateObservation(db, contentHash, windowMs = 3e4) {
14
286
  }
15
287
  function createObservation(db, memorySessionId, project, type, title, subtitle, text, narrative, facts, concepts, filesRead, filesModified, promptNumber, contentHash = null, discoveryTokens = 0) {
16
288
  const now = /* @__PURE__ */ new Date();
289
+ const safeTitle = redactSecrets(title);
290
+ const safeText = text ? redactSecrets(text) : text;
291
+ const safeNarrative = narrative ? redactSecrets(narrative) : narrative;
292
+ const autoCategory = categorize({
293
+ type,
294
+ title: safeTitle,
295
+ text: safeText,
296
+ narrative: safeNarrative,
297
+ concepts,
298
+ filesModified,
299
+ filesRead
300
+ });
17
301
  const result = db.run(
18
302
  `INSERT INTO observations
19
- (memory_session_id, project, type, title, subtitle, text, narrative, facts, concepts, files_read, files_modified, prompt_number, created_at, created_at_epoch, content_hash, discovery_tokens)
20
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
21
- [memorySessionId, project, type, title, subtitle, text, narrative, facts, concepts, filesRead, filesModified, promptNumber, now.toISOString(), now.getTime(), contentHash, discoveryTokens]
303
+ (memory_session_id, project, type, title, subtitle, text, narrative, facts, concepts, files_read, files_modified, prompt_number, created_at, created_at_epoch, content_hash, discovery_tokens, auto_category)
304
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
305
+ [memorySessionId, project, type, safeTitle, subtitle, safeText, safeNarrative, facts, concepts, filesRead, filesModified, promptNumber, now.toISOString(), now.getTime(), contentHash, discoveryTokens, autoCategory]
22
306
  );
23
307
  return Number(result.lastInsertRowid);
24
308
  }
@@ -30,16 +314,16 @@ function getObservationsBySession(db, memorySessionId) {
30
314
  }
31
315
  function getObservationsByProject(db, project, limit = 100) {
32
316
  const query = db.query(
33
- "SELECT * FROM observations WHERE project = ? ORDER BY created_at_epoch DESC LIMIT ?"
317
+ "SELECT * FROM observations WHERE project = ? ORDER BY created_at_epoch DESC, id DESC LIMIT ?"
34
318
  );
35
319
  return query.all(project, limit);
36
320
  }
37
321
  function searchObservations(db, searchTerm, project) {
38
322
  const sql = project ? `SELECT * FROM observations
39
323
  WHERE project = ? AND (title LIKE ? ESCAPE '\\' OR text LIKE ? ESCAPE '\\' OR narrative LIKE ? ESCAPE '\\')
40
- ORDER BY created_at_epoch DESC` : `SELECT * FROM observations
324
+ ORDER BY created_at_epoch DESC, id DESC` : `SELECT * FROM observations
41
325
  WHERE title LIKE ? ESCAPE '\\' OR text LIKE ? ESCAPE '\\' OR narrative LIKE ? ESCAPE '\\'
42
- ORDER BY created_at_epoch DESC`;
326
+ ORDER BY created_at_epoch DESC, id DESC`;
43
327
  const pattern = `%${escapeLikePattern(searchTerm)}%`;
44
328
  const query = db.query(sql);
45
329
  if (project) {
@@ -72,21 +356,32 @@ function consolidateObservations(db, project, options = {}) {
72
356
  ORDER BY cnt DESC
73
357
  `).all(project, minGroupSize);
74
358
  if (groups.length === 0) return { merged: 0, removed: 0 };
75
- let totalMerged = 0;
76
- let totalRemoved = 0;
359
+ if (options.dryRun) {
360
+ let totalMerged = 0;
361
+ let totalRemoved = 0;
362
+ for (const group of groups) {
363
+ const obsIds = group.ids.split(",").map(Number);
364
+ const placeholders = obsIds.map(() => "?").join(",");
365
+ const count = db.query(
366
+ `SELECT COUNT(*) as cnt FROM observations WHERE id IN (${placeholders})`
367
+ ).get(...obsIds)?.cnt || 0;
368
+ if (count >= minGroupSize) {
369
+ totalMerged += 1;
370
+ totalRemoved += count - 1;
371
+ }
372
+ }
373
+ return { merged: totalMerged, removed: totalRemoved };
374
+ }
77
375
  const runConsolidation = db.transaction(() => {
376
+ let merged = 0;
377
+ let removed = 0;
78
378
  for (const group of groups) {
79
379
  const obsIds = group.ids.split(",").map(Number);
80
380
  const placeholders = obsIds.map(() => "?").join(",");
81
381
  const observations = db.query(
82
- `SELECT * FROM observations WHERE id IN (${placeholders}) ORDER BY created_at_epoch DESC`
382
+ `SELECT * FROM observations WHERE id IN (${placeholders}) ORDER BY created_at_epoch DESC, id DESC`
83
383
  ).all(...obsIds);
84
384
  if (observations.length < minGroupSize) continue;
85
- if (options.dryRun) {
86
- totalMerged += 1;
87
- totalRemoved += observations.length - 1;
88
- continue;
89
- }
90
385
  const keeper = observations[0];
91
386
  const others = observations.slice(1);
92
387
  const uniqueTexts = /* @__PURE__ */ new Set();
@@ -99,18 +394,18 @@ function consolidateObservations(db, project, options = {}) {
99
394
  const consolidatedText = Array.from(uniqueTexts).join("\n---\n").substring(0, 1e5);
100
395
  db.run(
101
396
  "UPDATE observations SET text = ?, title = ? WHERE id = ?",
102
- [consolidatedText, `[consolidato x${observations.length}] ${keeper.title}`, keeper.id]
397
+ [consolidatedText, `[consolidated x${observations.length}] ${keeper.title}`, keeper.id]
103
398
  );
104
399
  const removeIds = others.map((o) => o.id);
105
400
  const removePlaceholders = removeIds.map(() => "?").join(",");
106
401
  db.run(`DELETE FROM observations WHERE id IN (${removePlaceholders})`, removeIds);
107
402
  db.run(`DELETE FROM observation_embeddings WHERE observation_id IN (${removePlaceholders})`, removeIds);
108
- totalMerged += 1;
109
- totalRemoved += removeIds.length;
403
+ merged += 1;
404
+ removed += removeIds.length;
110
405
  }
406
+ return { merged, removed };
111
407
  });
112
- runConsolidation();
113
- return { merged: totalMerged, removed: totalRemoved };
408
+ return runConsolidation();
114
409
  }
115
410
  export {
116
411
  consolidateObservations,
@@ -19,7 +19,7 @@ function getPromptsBySession(db, contentSessionId) {
19
19
  }
20
20
  function getPromptsByProject(db, project, limit = 100) {
21
21
  const query = db.query(
22
- "SELECT * FROM prompts WHERE project = ? ORDER BY created_at_epoch DESC LIMIT ?"
22
+ "SELECT * FROM prompts WHERE project = ? ORDER BY created_at_epoch DESC, id DESC LIMIT ?"
23
23
  );
24
24
  return query.all(project, limit);
25
25
  }
@@ -8,7 +8,7 @@ function escapeLikePattern(input) {
8
8
  }
9
9
  function sanitizeFTS5Query(query) {
10
10
  const trimmed = query.length > 1e4 ? query.substring(0, 1e4) : query;
11
- const terms = trimmed.replace(/[""]/g, "").split(/\s+/).filter((t) => t.length > 0).slice(0, 100).map((t) => `"${t}"`);
11
+ const terms = trimmed.replace(/[""\u0022]/g, "").split(/\s+/).filter((t) => t.length > 0).slice(0, 100).map((t) => `"${t}"`);
12
12
  return terms.join(" ");
13
13
  }
14
14
  function searchObservationsFTS(db, query, filters = {}) {
@@ -105,7 +105,7 @@ function searchObservationsLIKE(db, query, filters = {}) {
105
105
  sql += " AND created_at_epoch <= ?";
106
106
  params.push(filters.dateEnd);
107
107
  }
108
- sql += " ORDER BY created_at_epoch DESC LIMIT ?";
108
+ sql += " ORDER BY created_at_epoch DESC, id DESC LIMIT ?";
109
109
  params.push(limit);
110
110
  const stmt = db.query(sql);
111
111
  return stmt.all(...params);
@@ -130,7 +130,7 @@ function searchSummariesFiltered(db, query, filters = {}) {
130
130
  sql += " AND created_at_epoch <= ?";
131
131
  params.push(filters.dateEnd);
132
132
  }
133
- sql += " ORDER BY created_at_epoch DESC LIMIT ?";
133
+ sql += " ORDER BY created_at_epoch DESC, id DESC LIMIT ?";
134
134
  params.push(limit);
135
135
  const stmt = db.query(sql);
136
136
  return stmt.all(...params);
@@ -140,7 +140,7 @@ function getObservationsByIds(db, ids) {
140
140
  const validIds = ids.filter((id) => typeof id === "number" && Number.isInteger(id) && id > 0).slice(0, 500);
141
141
  if (validIds.length === 0) return [];
142
142
  const placeholders = validIds.map(() => "?").join(",");
143
- const sql = `SELECT * FROM observations WHERE id IN (${placeholders}) ORDER BY created_at_epoch DESC`;
143
+ const sql = `SELECT * FROM observations WHERE id IN (${placeholders}) ORDER BY created_at_epoch DESC, id DESC`;
144
144
  const stmt = db.query(sql);
145
145
  return stmt.all(...validIds);
146
146
  }
@@ -152,11 +152,11 @@ function getTimeline(db, anchorId, depthBefore = 5, depthAfter = 5) {
152
152
  const beforeStmt = db.query(`
153
153
  SELECT id, 'observation' as type, title, text as content, project, created_at, created_at_epoch
154
154
  FROM observations
155
- WHERE created_at_epoch < ?
156
- ORDER BY created_at_epoch DESC
155
+ WHERE (created_at_epoch < ? OR (created_at_epoch = ? AND id < ?))
156
+ ORDER BY created_at_epoch DESC, id DESC
157
157
  LIMIT ?
158
158
  `);
159
- const before = beforeStmt.all(anchorEpoch, depthBefore).reverse();
159
+ const before = beforeStmt.all(anchorEpoch, anchorEpoch, anchorId, depthBefore).reverse();
160
160
  const selfStmt = db.query(`
161
161
  SELECT id, 'observation' as type, title, text as content, project, created_at, created_at_epoch
162
162
  FROM observations WHERE id = ?
@@ -165,34 +165,46 @@ function getTimeline(db, anchorId, depthBefore = 5, depthAfter = 5) {
165
165
  const afterStmt = db.query(`
166
166
  SELECT id, 'observation' as type, title, text as content, project, created_at, created_at_epoch
167
167
  FROM observations
168
- WHERE created_at_epoch > ?
169
- ORDER BY created_at_epoch ASC
168
+ WHERE (created_at_epoch > ? OR (created_at_epoch = ? AND id > ?))
169
+ ORDER BY created_at_epoch ASC, id ASC
170
170
  LIMIT ?
171
171
  `);
172
- const after = afterStmt.all(anchorEpoch, depthAfter);
172
+ const after = afterStmt.all(anchorEpoch, anchorEpoch, anchorId, depthAfter);
173
173
  return [...before, ...self, ...after];
174
174
  }
175
175
  function getProjectStats(db, project) {
176
- const obsStmt = db.query("SELECT COUNT(*) as count FROM observations WHERE project = ?");
177
- const sumStmt = db.query("SELECT COUNT(*) as count FROM summaries WHERE project = ?");
178
- const sesStmt = db.query("SELECT COUNT(*) as count FROM sessions WHERE project = ?");
179
- const prmStmt = db.query("SELECT COUNT(*) as count FROM prompts WHERE project = ?");
180
- const discoveryStmt = db.query(
181
- "SELECT COALESCE(SUM(discovery_tokens), 0) as total FROM observations WHERE project = ?"
182
- );
183
- const discoveryTokens = discoveryStmt.get(project)?.total || 0;
184
- const readStmt = db.query(
185
- `SELECT COALESCE(SUM(
186
- CAST((LENGTH(COALESCE(title, '')) + LENGTH(COALESCE(narrative, ''))) / 4 AS INTEGER)
187
- ), 0) as total FROM observations WHERE project = ?`
188
- );
189
- const readTokens = readStmt.get(project)?.total || 0;
176
+ const sql = `
177
+ WITH
178
+ obs_stats AS (
179
+ SELECT
180
+ COUNT(*) as count,
181
+ COALESCE(SUM(discovery_tokens), 0) as discovery_tokens,
182
+ COALESCE(SUM(
183
+ CAST((LENGTH(COALESCE(title, '')) + LENGTH(COALESCE(narrative, ''))) / 4 AS INTEGER)
184
+ ), 0) as read_tokens
185
+ FROM observations WHERE project = ?
186
+ ),
187
+ sum_count AS (SELECT COUNT(*) as count FROM summaries WHERE project = ?),
188
+ ses_count AS (SELECT COUNT(*) as count FROM sessions WHERE project = ?),
189
+ prm_count AS (SELECT COUNT(*) as count FROM prompts WHERE project = ?)
190
+ SELECT
191
+ obs_stats.count as observations,
192
+ obs_stats.discovery_tokens,
193
+ obs_stats.read_tokens,
194
+ sum_count.count as summaries,
195
+ ses_count.count as sessions,
196
+ prm_count.count as prompts
197
+ FROM obs_stats, sum_count, ses_count, prm_count
198
+ `;
199
+ const row = db.query(sql).get(project, project, project, project);
200
+ const discoveryTokens = row?.discovery_tokens || 0;
201
+ const readTokens = row?.read_tokens || 0;
190
202
  const savings = Math.max(0, discoveryTokens - readTokens);
191
203
  return {
192
- observations: obsStmt.get(project)?.count || 0,
193
- summaries: sumStmt.get(project)?.count || 0,
194
- sessions: sesStmt.get(project)?.count || 0,
195
- prompts: prmStmt.get(project)?.count || 0,
204
+ observations: row?.observations || 0,
205
+ summaries: row?.summaries || 0,
206
+ sessions: row?.sessions || 0,
207
+ prompts: row?.prompts || 0,
196
208
  tokenEconomics: { discoveryTokens, readTokens, savings }
197
209
  };
198
210
  }
@@ -200,7 +212,7 @@ function getStaleObservations(db, project) {
200
212
  const rows = db.query(`
201
213
  SELECT * FROM observations
202
214
  WHERE project = ? AND files_modified IS NOT NULL AND files_modified != ''
203
- ORDER BY created_at_epoch DESC
215
+ ORDER BY created_at_epoch DESC, id DESC
204
216
  LIMIT 500
205
217
  `).all(project);
206
218
  const staleObs = [];
@@ -15,21 +15,21 @@ function createSummary(db, sessionId, project, request, investigated, learned, c
15
15
  return Number(result.lastInsertRowid);
16
16
  }
17
17
  function getSummaryBySession(db, sessionId) {
18
- const query = db.query("SELECT * FROM summaries WHERE session_id = ? ORDER BY created_at_epoch DESC LIMIT 1");
18
+ const query = db.query("SELECT * FROM summaries WHERE session_id = ? ORDER BY created_at_epoch DESC, id DESC LIMIT 1");
19
19
  return query.get(sessionId);
20
20
  }
21
21
  function getSummariesByProject(db, project, limit = 50) {
22
22
  const query = db.query(
23
- "SELECT * FROM summaries WHERE project = ? ORDER BY created_at_epoch DESC LIMIT ?"
23
+ "SELECT * FROM summaries WHERE project = ? ORDER BY created_at_epoch DESC, id DESC LIMIT ?"
24
24
  );
25
25
  return query.all(project, limit);
26
26
  }
27
27
  function searchSummaries(db, searchTerm, project) {
28
28
  const sql = project ? `SELECT * FROM summaries
29
29
  WHERE project = ? AND (request LIKE ? ESCAPE '\\' OR learned LIKE ? ESCAPE '\\' OR completed LIKE ? ESCAPE '\\' OR notes LIKE ? ESCAPE '\\')
30
- ORDER BY created_at_epoch DESC` : `SELECT * FROM summaries
30
+ ORDER BY created_at_epoch DESC, id DESC` : `SELECT * FROM summaries
31
31
  WHERE request LIKE ? ESCAPE '\\' OR learned LIKE ? ESCAPE '\\' OR completed LIKE ? ESCAPE '\\' OR notes LIKE ? ESCAPE '\\'
32
- ORDER BY created_at_epoch DESC`;
32
+ ORDER BY created_at_epoch DESC, id DESC`;
33
33
  const pattern = `%${escapeLikePattern(searchTerm)}%`;
34
34
  const query = db.query(sql);
35
35
  if (project) {