agentic-qe 3.7.14 → 3.7.16

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 (55) hide show
  1. package/.claude/helpers/brain-checkpoint.cjs +11 -0
  2. package/.claude/skills/skills-manifest.json +1 -1
  3. package/CHANGELOG.md +49 -0
  4. package/dist/cli/bundle.js +1260 -528
  5. package/dist/cli/commands/prove.d.ts +60 -0
  6. package/dist/cli/commands/prove.js +167 -0
  7. package/dist/cli/handlers/brain-handler.js +2 -1
  8. package/dist/cli/index.js +2 -0
  9. package/dist/domains/test-generation/coordinator.js +6 -4
  10. package/dist/domains/test-generation/pattern-injection/edge-case-injector.d.ts +6 -0
  11. package/dist/domains/test-generation/pattern-injection/edge-case-injector.js +30 -0
  12. package/dist/feedback/feedback-loop.d.ts +5 -0
  13. package/dist/feedback/feedback-loop.js +12 -0
  14. package/dist/feedback/index.d.ts +1 -1
  15. package/dist/feedback/index.js +1 -1
  16. package/dist/kernel/hnsw-adapter.d.ts +3 -0
  17. package/dist/kernel/hnsw-adapter.js +11 -1
  18. package/dist/kernel/unified-memory-schemas.d.ts +3 -3
  19. package/dist/kernel/unified-memory-schemas.js +28 -1
  20. package/dist/kernel/unified-memory.js +57 -0
  21. package/dist/learning/aqe-learning-engine.js +2 -1
  22. package/dist/learning/daily-log.d.ts +43 -0
  23. package/dist/learning/daily-log.js +91 -0
  24. package/dist/learning/experience-capture-middleware.js +24 -0
  25. package/dist/learning/experience-capture.d.ts +42 -0
  26. package/dist/learning/experience-capture.js +94 -4
  27. package/dist/learning/index.d.ts +4 -0
  28. package/dist/learning/index.js +8 -0
  29. package/dist/learning/opd-remediation.d.ts +55 -0
  30. package/dist/learning/opd-remediation.js +130 -0
  31. package/dist/learning/pattern-lifecycle.d.ts +12 -1
  32. package/dist/learning/pattern-lifecycle.js +18 -2
  33. package/dist/learning/pattern-store.d.ts +12 -4
  34. package/dist/learning/pattern-store.js +178 -19
  35. package/dist/learning/qe-hooks.d.ts +1 -0
  36. package/dist/learning/qe-hooks.js +30 -0
  37. package/dist/learning/qe-patterns.d.ts +6 -0
  38. package/dist/learning/qe-patterns.js +10 -1
  39. package/dist/learning/sqlite-persistence.d.ts +43 -0
  40. package/dist/learning/sqlite-persistence.js +237 -1
  41. package/dist/learning/token-tracker.js +4 -0
  42. package/dist/mcp/bundle.js +836 -48
  43. package/dist/mcp/handlers/core-handlers.d.ts +5 -0
  44. package/dist/mcp/handlers/core-handlers.js +11 -0
  45. package/dist/mcp/handlers/handler-factory.js +92 -11
  46. package/dist/mcp/index.d.ts +1 -0
  47. package/dist/mcp/index.js +2 -0
  48. package/dist/mcp/tool-scoping.d.ts +36 -0
  49. package/dist/mcp/tool-scoping.js +129 -0
  50. package/dist/routing/routing-feedback.d.ts +5 -0
  51. package/dist/routing/routing-feedback.js +29 -3
  52. package/dist/sync/pull-agent.js +2 -1
  53. package/dist/test-scheduling/pipeline.d.ts +7 -0
  54. package/dist/test-scheduling/pipeline.js +9 -0
  55. package/package.json +1 -1
@@ -0,0 +1,130 @@
1
+ /**
2
+ * OPD (Observe-Plan-Decide) Remediation Hints
3
+ *
4
+ * When patterns have negative rewards (failures, flakiness), generate
5
+ * actionable hints: "bad because X, fix by Y"
6
+ *
7
+ * Categories:
8
+ * - flaky: intermittent failures (20-80% fail rate, 3+ executions)
9
+ * - false-positive: consistently broken (80%+ fail rate)
10
+ * - outdated: was working, now fails (recent regression)
11
+ * - wrong-scope: vague description + high failure rate
12
+ * - missing-context: recurring keywords in failure feedback
13
+ */
14
+ // ============================================================================
15
+ // Hint Generation
16
+ // ============================================================================
17
+ /**
18
+ * Generate remediation hints for a failed pattern based on its execution history.
19
+ *
20
+ * Analyzes execution records to classify the failure mode and produce
21
+ * actionable suggestions for fixing the pattern.
22
+ */
23
+ export function generateRemediationHints(pattern, executionHistory, config) {
24
+ const hints = [];
25
+ const maxHints = config?.maxHintsPerPattern ?? 3;
26
+ const totalCount = executionHistory.length;
27
+ if (totalCount === 0)
28
+ return hints;
29
+ const failCount = executionHistory.filter((e) => !e.success).length;
30
+ const failRate = failCount / totalCount;
31
+ // Category 1: Flaky (intermittent failures — 20-80% fail rate, 3+ runs)
32
+ if (failRate > 0.2 && failRate < 0.8 && totalCount >= 3) {
33
+ hints.push({
34
+ patternId: pattern.id,
35
+ observation: `Pattern "${pattern.name}" fails ${(failRate * 100).toFixed(0)}% of the time (${failCount}/${totalCount} executions)`,
36
+ diagnosis: 'Intermittent failures suggest timing dependencies, external service flakiness, or non-deterministic behavior',
37
+ suggestion: 'Add retry logic, mock external dependencies, or add explicit waits for async operations',
38
+ confidence: Math.min(0.9, 0.5 + totalCount * 0.05),
39
+ category: 'flaky',
40
+ });
41
+ }
42
+ // Category 2: False positive (always/almost always fails)
43
+ if (failRate >= 0.8 && totalCount >= 2) {
44
+ hints.push({
45
+ patternId: pattern.id,
46
+ observation: `Pattern "${pattern.name}" fails ${(failRate * 100).toFixed(0)}% of executions — effectively broken`,
47
+ diagnosis: 'Consistent failures indicate the pattern logic is incorrect or the target code changed',
48
+ suggestion: 'Review the pattern against current code. Consider quarantining and creating a replacement pattern.',
49
+ confidence: 0.85,
50
+ category: 'false-positive',
51
+ });
52
+ }
53
+ // Category 3: Outdated (was working, now failing — regression)
54
+ if (totalCount >= 5) {
55
+ const recentFails = executionHistory.slice(-3).filter((e) => !e.success).length;
56
+ const earlySuccesses = executionHistory
57
+ .slice(0, Math.max(1, totalCount - 3))
58
+ .filter((e) => e.success).length;
59
+ if (recentFails >= 2 && earlySuccesses >= 2) {
60
+ hints.push({
61
+ patternId: pattern.id,
62
+ observation: `Pattern "${pattern.name}" worked previously but now fails consistently`,
63
+ diagnosis: 'Recent code changes likely broke compatibility with this pattern',
64
+ suggestion: 'Update pattern to match current code structure. Check git log for recent changes to affected files.',
65
+ confidence: 0.8,
66
+ category: 'outdated',
67
+ });
68
+ }
69
+ }
70
+ // Category 4: Wrong scope (too broad — vague description + high failure)
71
+ if (pattern.description && pattern.description.length < 20 && failRate > 0.3) {
72
+ hints.push({
73
+ patternId: pattern.id,
74
+ observation: `Pattern "${pattern.name}" has a vague description and high failure rate`,
75
+ diagnosis: 'Pattern may be too broadly scoped — matching contexts where it does not apply',
76
+ suggestion: 'Narrow the pattern scope by adding specific tags, domain constraints, or more detailed matching criteria',
77
+ confidence: 0.6,
78
+ category: 'wrong-scope',
79
+ });
80
+ }
81
+ // Category 5: Missing context (recurring keywords in failure feedback)
82
+ const feedbackMessages = executionHistory
83
+ .filter((e) => !e.success && e.feedback)
84
+ .map((e) => e.feedback)
85
+ .slice(-3);
86
+ if (feedbackMessages.length > 0) {
87
+ const commonWords = findCommonKeywords(feedbackMessages);
88
+ if (commonWords.length > 0) {
89
+ hints.push({
90
+ patternId: pattern.id,
91
+ observation: `Failure feedback contains recurring themes: ${commonWords.join(', ')}`,
92
+ diagnosis: `Common failure keywords suggest a systematic issue: ${commonWords.slice(0, 3).join(', ')}`,
93
+ suggestion: `Address the recurring "${commonWords[0]}" issue in the pattern logic or preconditions`,
94
+ confidence: 0.65,
95
+ category: 'missing-context',
96
+ });
97
+ }
98
+ }
99
+ return hints.slice(0, maxHints);
100
+ }
101
+ // ============================================================================
102
+ // Keyword Extraction (Internal)
103
+ // ============================================================================
104
+ const STOP_WORDS = new Set([
105
+ 'the', 'a', 'an', 'is', 'was', 'in', 'to', 'for', 'of',
106
+ 'and', 'or', 'not', 'with', 'test', 'error',
107
+ ]);
108
+ /**
109
+ * Find common keywords across failure feedback messages.
110
+ * Returns words that appear in at least 2 distinct messages, sorted by frequency.
111
+ */
112
+ export function findCommonKeywords(feedbacks) {
113
+ const wordCounts = new Map();
114
+ for (const feedback of feedbacks) {
115
+ const words = feedback
116
+ .toLowerCase()
117
+ .split(/\W+/)
118
+ .filter((w) => w.length > 2 && !STOP_WORDS.has(w));
119
+ const uniqueWords = new Set(words);
120
+ for (const word of uniqueWords) {
121
+ wordCounts.set(word, (wordCounts.get(word) ?? 0) + 1);
122
+ }
123
+ }
124
+ return Array.from(wordCounts.entries())
125
+ .filter(([, count]) => count >= 2)
126
+ .sort((a, b) => b[1] - a[1])
127
+ .map(([word]) => word)
128
+ .slice(0, 5);
129
+ }
130
+ //# sourceMappingURL=opd-remediation.js.map
@@ -9,7 +9,7 @@
9
9
  * - Confidence decay over time
10
10
  */
11
11
  import type { Database as DatabaseType } from 'better-sqlite3';
12
- import type { QEDomain, QEPatternType } from './qe-patterns.js';
12
+ import { type QEDomain, type QEPatternType } from './qe-patterns.js';
13
13
  import { AsymmetricLearningEngine, type AsymmetricLearningConfig } from './asymmetric-learning.js';
14
14
  import type { WitnessChain } from '../audit/witness-chain.js';
15
15
  /**
@@ -32,6 +32,8 @@ export interface PatternLifecycleConfig {
32
32
  minActiveConfidence: number;
33
33
  /** Maximum age in days before automatic deprecation review */
34
34
  maxAgeForActivePatterns: number;
35
+ /** Days of activity window required for pattern promotion (default: 30) */
36
+ promotionActivityWindowDays: number;
35
37
  /** ADR-061: Asymmetric learning config */
36
38
  asymmetricLearning: Partial<AsymmetricLearningConfig>;
37
39
  }
@@ -75,6 +77,7 @@ export interface PromotionCheckResult {
75
77
  meetsRewardThreshold: boolean;
76
78
  meetsOccurrenceThreshold: boolean;
77
79
  meetsSuccessRateThreshold: boolean;
80
+ meetsActivityWindow: boolean;
78
81
  currentReward: number;
79
82
  currentOccurrences: number;
80
83
  currentSuccessRate: number;
@@ -165,6 +168,14 @@ export declare class PatternLifecycleManager {
165
168
  promoted: number;
166
169
  checked: number;
167
170
  };
171
+ /**
172
+ * Run a promotion sweep — convenience alias for pre-compaction hook.
173
+ * Iterates short-term patterns, checks promotion criteria, promotes eligible ones.
174
+ */
175
+ runPromotionSweep(): {
176
+ promoted: number;
177
+ checked: number;
178
+ };
168
179
  /**
169
180
  * Check if a pattern should be deprecated
170
181
  */
@@ -8,6 +8,7 @@
8
8
  * - Quality feedback loops
9
9
  * - Confidence decay over time
10
10
  */
11
+ import { PROMOTION_THRESHOLD } from './qe-patterns.js';
11
12
  import { AsymmetricLearningEngine } from './asymmetric-learning.js';
12
13
  import { safeJsonParse } from '../shared/safe-json.js';
13
14
  import { LoggerFactory } from '../logging/index.js';
@@ -17,13 +18,14 @@ const logger = LoggerFactory.create('pattern-lifecycle');
17
18
  */
18
19
  export const DEFAULT_LIFECYCLE_CONFIG = {
19
20
  promotionRewardThreshold: 0.7,
20
- promotionMinOccurrences: 2,
21
+ promotionMinOccurrences: PROMOTION_THRESHOLD,
21
22
  promotionMinSuccessRate: 0.7,
22
23
  deprecationFailureThreshold: 3,
23
24
  staleDaysThreshold: 30,
24
25
  confidenceDecayRate: 0.01, // 1% per day
25
26
  minActiveConfidence: 0.3,
26
27
  maxAgeForActivePatterns: 90,
28
+ promotionActivityWindowDays: 30,
27
29
  asymmetricLearning: {},
28
30
  };
29
31
  // ============================================================================
@@ -306,6 +308,7 @@ Pattern extracted from ${exp.count} successful experiences.`;
306
308
  meetsRewardThreshold: false,
307
309
  meetsOccurrenceThreshold: false,
308
310
  meetsSuccessRateThreshold: false,
311
+ meetsActivityWindow: false,
309
312
  currentReward: 0,
310
313
  currentOccurrences: 0,
311
314
  currentSuccessRate: 0,
@@ -315,11 +318,17 @@ Pattern extracted from ${exp.count} successful experiences.`;
315
318
  const meetsReward = avgReward >= this.config.promotionRewardThreshold;
316
319
  const meetsOccurrences = pattern.usageCount >= this.config.promotionMinOccurrences;
317
320
  const meetsSuccessRate = pattern.successRate >= this.config.promotionMinSuccessRate;
321
+ // Temporal window: require activity within configured window to prevent
322
+ // promoting stale patterns that haven't been validated recently
323
+ const PROMOTION_ACTIVITY_WINDOW_MS = this.config.promotionActivityWindowDays * 24 * 60 * 60 * 1000;
324
+ const lastActivity = pattern.lastUsedAt?.getTime() ?? pattern.createdAt.getTime();
325
+ const meetsActivityWindow = (Date.now() - lastActivity) < PROMOTION_ACTIVITY_WINDOW_MS;
318
326
  return {
319
- shouldPromote: pattern.tier === 'short-term' && meetsReward && meetsOccurrences && meetsSuccessRate,
327
+ shouldPromote: pattern.tier === 'short-term' && meetsReward && meetsOccurrences && meetsSuccessRate && meetsActivityWindow,
320
328
  meetsRewardThreshold: meetsReward,
321
329
  meetsOccurrenceThreshold: meetsOccurrences,
322
330
  meetsSuccessRateThreshold: meetsSuccessRate,
331
+ meetsActivityWindow,
323
332
  currentReward: avgReward,
324
333
  currentOccurrences: pattern.usageCount,
325
334
  currentSuccessRate: pattern.successRate,
@@ -360,6 +369,13 @@ Pattern extracted from ${exp.count} successful experiences.`;
360
369
  }
361
370
  return { promoted, checked: shortTermPatterns.length };
362
371
  }
372
+ /**
373
+ * Run a promotion sweep — convenience alias for pre-compaction hook.
374
+ * Iterates short-term patterns, checks promotion criteria, promotes eligible ones.
375
+ */
376
+ runPromotionSweep() {
377
+ return this.promoteEligiblePatterns();
378
+ }
363
379
  // ============================================================================
364
380
  // Pattern Deprecation
365
381
  // ============================================================================
@@ -183,6 +183,7 @@ export declare class PatternStore implements IPatternStore {
183
183
  private initialized;
184
184
  private cleanupTimer?;
185
185
  private sqliteStore;
186
+ private loadingPromise;
186
187
  private patternCache;
187
188
  private domainIndex;
188
189
  private typeIndex;
@@ -193,9 +194,12 @@ export declare class PatternStore implements IPatternStore {
193
194
  private stats;
194
195
  constructor(memory: MemoryBackend, config?: Partial<PatternStoreConfig>);
195
196
  /**
196
- * Set SQLite persistence delegate for delete/promote operations.
197
- * When set, PatternStore will forward these operations to SQLite
198
- * in addition to updating the in-memory cache.
197
+ * Set SQLite persistence delegate and load patterns into memory.
198
+ *
199
+ * When set, PatternStore will:
200
+ * 1. Load existing patterns from SQLite into the in-memory cache
201
+ * 2. Forward create/delete/promote operations to SQLite for persistence
202
+ * 3. Persist embeddings alongside patterns on store()
199
203
  */
200
204
  setSqliteStore(store: import('./sqlite-persistence.js').SQLitePatternStore): void;
201
205
  /**
@@ -220,7 +224,11 @@ export declare class PatternStore implements IPatternStore {
220
224
  */
221
225
  private initializeHNSWInternal;
222
226
  /**
223
- * Load existing patterns from memory with timeout protection
227
+ * Load existing patterns from SQLite into in-memory cache.
228
+ *
229
+ * Previously this was a no-op after Issue #258 removed kv_store duplication,
230
+ * but that left 15,634 SQLite patterns invisible to search on every restart.
231
+ * Now properly loads from SQLitePatternStore when wired.
224
232
  */
225
233
  private loadPatterns;
226
234
  /**
@@ -8,7 +8,7 @@
8
8
  import { v4 as uuidv4 } from 'uuid';
9
9
  import { ok, err } from '../shared/types/index.js';
10
10
  import { toErrorMessage, toError } from '../shared/error-utils.js';
11
- import { calculateQualityScore, shouldPromotePattern, validateQEPattern, mapQEDomainToAQE, } from './qe-patterns.js';
11
+ import { calculateQualityScore, shouldPromotePattern, validateQEPattern, mapQEDomainToAQE, PROMOTION_THRESHOLD, } from './qe-patterns.js';
12
12
  /**
13
13
  * Default pattern store configuration
14
14
  */
@@ -21,7 +21,7 @@ export const DEFAULT_PATTERN_STORE_CONFIG = {
21
21
  efSearch: 100,
22
22
  maxElements: 50000,
23
23
  },
24
- promotionThreshold: 3,
24
+ promotionThreshold: PROMOTION_THRESHOLD,
25
25
  minConfidence: 0.3,
26
26
  maxPatternsPerDomain: 5000,
27
27
  autoCleanup: true,
@@ -55,6 +55,7 @@ export class PatternStore {
55
55
  cleanupTimer;
56
56
  // Optional SQLite persistence delegate for delete/promote
57
57
  sqliteStore = null;
58
+ loadingPromise = null;
58
59
  // In-memory caches for fast access
59
60
  patternCache = new Map();
60
61
  domainIndex = new Map();
@@ -75,12 +76,23 @@ export class PatternStore {
75
76
  this.config = { ...DEFAULT_PATTERN_STORE_CONFIG, ...config };
76
77
  }
77
78
  /**
78
- * Set SQLite persistence delegate for delete/promote operations.
79
- * When set, PatternStore will forward these operations to SQLite
80
- * in addition to updating the in-memory cache.
79
+ * Set SQLite persistence delegate and load patterns into memory.
80
+ *
81
+ * When set, PatternStore will:
82
+ * 1. Load existing patterns from SQLite into the in-memory cache
83
+ * 2. Forward create/delete/promote operations to SQLite for persistence
84
+ * 3. Persist embeddings alongside patterns on store()
81
85
  */
82
86
  setSqliteStore(store) {
83
87
  this.sqliteStore = store;
88
+ // Load patterns from SQLite if we're already initialized
89
+ // (setSqliteStore is called after initialize() in QEReasoningBank)
90
+ // Store promise so concurrent store/search calls can await it
91
+ if (this.initialized) {
92
+ this.loadingPromise = this.loadPatterns().catch((e) => console.warn('[PatternStore] Failed to load patterns after setSqliteStore:', e)).finally(() => {
93
+ this.loadingPromise = null;
94
+ });
95
+ }
84
96
  }
85
97
  /**
86
98
  * Initialize the pattern store
@@ -161,6 +173,47 @@ export class PatternStore {
161
173
  const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('HNSW init timeout')), timeoutMs));
162
174
  await Promise.race([initPromise, timeoutPromise]);
163
175
  this.hnswAvailable = this.hnswIndex.isNativeAvailable();
176
+ // Load existing embeddings from SQLite into HNSW index (capped to prevent timeout)
177
+ if (this.sqliteStore) {
178
+ try {
179
+ const embeddings = this.sqliteStore.getAllEmbeddings();
180
+ const maxBootstrap = this.config.hnsw.maxElements;
181
+ let loaded = 0;
182
+ for (const { patternId, embedding } of embeddings) {
183
+ if (loaded >= maxBootstrap)
184
+ break;
185
+ if (!embedding || embedding.length !== this.config.embeddingDimension)
186
+ continue;
187
+ const pattern = this.patternCache.get(patternId);
188
+ if (!pattern)
189
+ continue;
190
+ try {
191
+ await this.hnswIndex.insert(patternId, embedding, {
192
+ filePath: pattern.patternType,
193
+ lineCoverage: pattern.confidence * 100,
194
+ branchCoverage: pattern.qualityScore * 100,
195
+ functionCoverage: 0,
196
+ statementCoverage: 0,
197
+ uncoveredLineCount: 0,
198
+ uncoveredBranchCount: 0,
199
+ riskScore: 1 - pattern.confidence,
200
+ lastUpdated: Date.now(),
201
+ totalLines: 0,
202
+ });
203
+ loaded++;
204
+ }
205
+ catch {
206
+ // Duplicate or invalid — skip
207
+ }
208
+ }
209
+ if (loaded > 0) {
210
+ console.log(`[PatternStore] Loaded ${loaded} embeddings from SQLite into HNSW`);
211
+ }
212
+ }
213
+ catch (error) {
214
+ console.warn('[PatternStore] Failed to load SQLite embeddings into HNSW:', toErrorMessage(error));
215
+ }
216
+ }
164
217
  console.log(`[PatternStore] HNSW lazy-initialized (native: ${this.hnswAvailable})`);
165
218
  }
166
219
  catch (error) {
@@ -170,13 +223,28 @@ export class PatternStore {
170
223
  }
171
224
  }
172
225
  /**
173
- * Load existing patterns from memory with timeout protection
226
+ * Load existing patterns from SQLite into in-memory cache.
227
+ *
228
+ * Previously this was a no-op after Issue #258 removed kv_store duplication,
229
+ * but that left 15,634 SQLite patterns invisible to search on every restart.
230
+ * Now properly loads from SQLitePatternStore when wired.
174
231
  */
175
232
  async loadPatterns() {
176
- // Patterns are loaded from qe_patterns table by SQLitePatternStore.
177
- // PatternStore's in-memory cache is populated via indexPattern() calls
178
- // from the ReasoningBank when it loads from the relational store.
179
- // Previously this loaded from kv_store, which duplicated storage (Issue #258).
233
+ if (!this.sqliteStore) {
234
+ return; // SQLite not wired yet will be loaded after setSqliteStore()
235
+ }
236
+ try {
237
+ const patterns = this.sqliteStore.getPatterns({ limit: 50000 });
238
+ for (const pattern of patterns) {
239
+ this.indexPattern(pattern);
240
+ }
241
+ if (patterns.length > 0) {
242
+ console.log(`[PatternStore] Loaded ${patterns.length} patterns from SQLite into memory cache`);
243
+ }
244
+ }
245
+ catch (error) {
246
+ console.warn('[PatternStore] Failed to load patterns from SQLite:', toErrorMessage(error));
247
+ }
180
248
  }
181
249
  /**
182
250
  * Index a pattern in local caches
@@ -193,8 +261,13 @@ export class PatternStore {
193
261
  this.typeIndex.set(pattern.patternType, new Set());
194
262
  }
195
263
  this.typeIndex.get(pattern.patternType).add(pattern.id);
196
- // Tier index
197
- this.tierIndex.get(pattern.tier).add(pattern.id);
264
+ // Tier index (defensive: coerce unexpected tier values to 'short-term')
265
+ const tier = (pattern.tier === 'long-term') ? 'long-term' : 'short-term';
266
+ if (pattern.tier !== tier) {
267
+ // Pattern has invalid tier from SQLite — store corrected copy in cache
268
+ pattern.tier = tier;
269
+ }
270
+ this.tierIndex.get(tier).add(pattern.id);
198
271
  }
199
272
  /**
200
273
  * Remove pattern from local indices
@@ -212,6 +285,9 @@ export class PatternStore {
212
285
  if (!this.initialized) {
213
286
  await this.initialize();
214
287
  }
288
+ if (this.loadingPromise) {
289
+ await this.loadingPromise;
290
+ }
215
291
  // Validate pattern
216
292
  const validation = validateQEPattern(pattern);
217
293
  if (!validation.valid) {
@@ -227,11 +303,17 @@ export class PatternStore {
227
303
  // Run cleanup for this domain
228
304
  await this.cleanupDomain(pattern.qeDomain);
229
305
  }
230
- // Patterns are persisted to qe_patterns table by SQLitePatternStore.
231
- // PatternStore only maintains in-memory cache + HNSW index for fast search.
232
- // Previously this wrote to kv_store, causing 229MB bloat (Issue #258).
233
- // Index locally
306
+ // Index in memory cache
234
307
  this.indexPattern(pattern);
308
+ // Persist to SQLite (pattern + embedding atomically)
309
+ if (this.sqliteStore) {
310
+ try {
311
+ this.sqliteStore.storePattern(pattern, pattern.embedding);
312
+ }
313
+ catch (error) {
314
+ console.warn(`[PatternStore] SQLite persist failed for ${pattern.id}:`, toErrorMessage(error));
315
+ }
316
+ }
235
317
  // Add to HNSW if embedding is available (lazy-load HNSW only when needed)
236
318
  if (pattern.embedding) {
237
319
  const hnsw = await this.ensureHNSW();
@@ -327,6 +409,9 @@ export class PatternStore {
327
409
  if (!this.initialized) {
328
410
  await this.initialize();
329
411
  }
412
+ if (this.loadingPromise) {
413
+ await this.loadingPromise;
414
+ }
330
415
  return this.patternCache.get(id) ?? null;
331
416
  }
332
417
  /**
@@ -336,6 +421,9 @@ export class PatternStore {
336
421
  if (!this.initialized) {
337
422
  await this.initialize();
338
423
  }
424
+ if (this.loadingPromise) {
425
+ await this.loadingPromise;
426
+ }
339
427
  const startTime = performance.now();
340
428
  const limit = options.limit || 10;
341
429
  const results = [];
@@ -362,10 +450,69 @@ export class PatternStore {
362
450
  }
363
451
  }
364
452
  }
453
+ // FTS5 hybrid search: blend BM25 text relevance with vector similarity
454
+ // 75% vector score + 25% FTS5 score for patterns found by both
455
+ if (typeof query === 'string' && query.trim() && this.sqliteStore) {
456
+ try {
457
+ const ftsResults = this.sqliteStore.searchFTS(query, limit * 2);
458
+ if (ftsResults.length > 0) {
459
+ const ftsScoreMap = new Map(ftsResults.map(r => [r.id, r.ftsScore]));
460
+ const existingIds = new Set(results.map(r => r.pattern.id));
461
+ // Boost existing vector results that also match FTS5
462
+ for (const result of results) {
463
+ const ftsScore = ftsScoreMap.get(result.pattern.id);
464
+ if (ftsScore !== undefined) {
465
+ result.score = 0.75 * result.score + 0.25 * ftsScore;
466
+ }
467
+ }
468
+ // Add FTS5-only results not already in vector results
469
+ for (const ftsResult of ftsResults) {
470
+ if (existingIds.has(ftsResult.id))
471
+ continue;
472
+ const pattern = await this.get(ftsResult.id);
473
+ if (pattern && this.matchesFilters(pattern, options)) {
474
+ const reuseInfo = this.calculateReuseInfo(pattern, ftsResult.ftsScore);
475
+ results.push({
476
+ pattern,
477
+ score: 0.5 * ftsResult.ftsScore, // FTS-only: exact keyword match is valuable
478
+ matchType: 'exact',
479
+ similarity: ftsResult.ftsScore,
480
+ canReuse: reuseInfo.canReuse,
481
+ estimatedTokenSavings: reuseInfo.estimatedTokenSavings,
482
+ reuseConfidence: reuseInfo.reuseConfidence,
483
+ });
484
+ }
485
+ }
486
+ }
487
+ }
488
+ catch {
489
+ // FTS5 unavailable, continue with text fallback
490
+ }
491
+ }
365
492
  // Text search fallback or additional
366
493
  if (typeof query === 'string' || results.length < limit) {
367
494
  const textResults = await this.searchByText(typeof query === 'string' ? query : '', options, limit - results.length);
368
- results.push(...textResults);
495
+ // Deduplicate: only add text results not already present
496
+ const existingIds = new Set(results.map(r => r.pattern.id));
497
+ for (const tr of textResults) {
498
+ if (!existingIds.has(tr.pattern.id)) {
499
+ results.push(tr);
500
+ }
501
+ }
502
+ }
503
+ // Apply temporal decay: boost recent patterns, penalize stale ones
504
+ // Half-life of 30 days — patterns used recently score higher
505
+ const TEMPORAL_HALF_LIFE_MS = 30 * 24 * 60 * 60 * 1000;
506
+ const now = Date.now();
507
+ for (const result of results) {
508
+ const lastUsed = result.pattern.lastUsedAt?.getTime() ?? result.pattern.createdAt.getTime();
509
+ const ageMs = now - lastUsed;
510
+ const decayFactor = Math.pow(0.5, ageMs / TEMPORAL_HALF_LIFE_MS);
511
+ // Only boost patterns that have been used — new untested patterns get neutral score
512
+ const effectiveDecay = result.pattern.usageCount > 0 ? decayFactor : 0.5;
513
+ // Multiplicative decay: preserves relative ordering from search scoring
514
+ // while penalizing stale patterns (decayFactor is 0-1)
515
+ result.score = result.score * (0.7 + 0.3 * effectiveDecay);
369
516
  }
370
517
  // Sort by score and limit
371
518
  results.sort((a, b) => b.score - a.score);
@@ -555,6 +702,15 @@ export class PatternStore {
555
702
  qualityScore,
556
703
  lastUsedAt: now,
557
704
  };
705
+ // Persist usage to SQLite
706
+ if (this.sqliteStore) {
707
+ try {
708
+ this.sqliteStore.recordUsage(id, success);
709
+ }
710
+ catch (error) {
711
+ console.warn(`[PatternStore] SQLite recordUsage failed for ${id}:`, toErrorMessage(error));
712
+ }
713
+ }
558
714
  // Check for promotion (ADR-052: shouldPromotePattern returns PromotionCheck object)
559
715
  const promotionCheck = shouldPromotePattern(updated);
560
716
  const shouldPromote = promotionCheck.meetsUsageCriteria &&
@@ -564,7 +720,7 @@ export class PatternStore {
564
720
  await this.promote(id);
565
721
  }
566
722
  else {
567
- // Update cache only - persistence handled by SQLitePatternStore
723
+ // Update in-memory cache
568
724
  this.patternCache.set(id, updated);
569
725
  }
570
726
  return ok(undefined);
@@ -699,7 +855,10 @@ export class PatternStore {
699
855
  }
700
856
  // Check for removal (short-term, old, low quality)
701
857
  if (pattern.tier === 'short-term') {
702
- const ageMs = Date.now() - pattern.createdAt.getTime();
858
+ const createdTime = pattern.createdAt instanceof Date
859
+ ? pattern.createdAt.getTime()
860
+ : new Date(pattern.createdAt).getTime();
861
+ const ageMs = Date.now() - createdTime;
703
862
  const isOld = ageMs > 7 * 24 * 60 * 60 * 1000; // 7 days
704
863
  const isLowQuality = pattern.qualityScore < 0.2;
705
864
  const isUnused = pattern.usageCount === 0 && ageMs > 24 * 60 * 60 * 1000; // 1 day
@@ -25,6 +25,7 @@ export declare const QE_HOOK_EVENTS: {
25
25
  readonly PatternLearned: "qe:pattern-learned";
26
26
  readonly PatternApplied: "qe:pattern-applied";
27
27
  readonly PatternPromoted: "qe:pattern-promoted";
28
+ readonly PreCompaction: "qe:pre-compaction";
28
29
  };
29
30
  export type QEHookEvent = (typeof QE_HOOK_EVENTS)[keyof typeof QE_HOOK_EVENTS];
30
31
  /**
@@ -34,6 +34,8 @@ export const QE_HOOK_EVENTS = {
34
34
  PatternLearned: 'qe:pattern-learned',
35
35
  PatternApplied: 'qe:pattern-applied',
36
36
  PatternPromoted: 'qe:pattern-promoted',
37
+ // Session lifecycle
38
+ PreCompaction: 'qe:pre-compaction',
37
39
  };
38
40
  // ============================================================================
39
41
  // QE Hook Handlers
@@ -434,6 +436,34 @@ export function createQEHookHandlers(reasoningBank) {
434
436
  data: { patternId, newTier },
435
437
  };
436
438
  },
439
+ // ========================================================================
440
+ // Session Lifecycle Hooks
441
+ // ========================================================================
442
+ [QE_HOOK_EVENTS.PreCompaction]: async (ctx) => {
443
+ const stats = { experiencesFlushed: 0, patternsPromoted: 0 };
444
+ // Flush pending experiences before context compaction
445
+ if (ctx.data?.experienceCaptureService) {
446
+ const service = ctx.data.experienceCaptureService;
447
+ const pendingCount = service.getPendingCount?.() ?? 0;
448
+ if (pendingCount > 0) {
449
+ const flushed = await service.flushPending?.();
450
+ stats.experiencesFlushed = flushed ?? pendingCount;
451
+ }
452
+ }
453
+ // Promote eligible patterns before compaction
454
+ if (ctx.data?.patternLifecycleManager) {
455
+ const manager = ctx.data.patternLifecycleManager;
456
+ const promotionResult = manager.runPromotionSweep?.();
457
+ if (promotionResult) {
458
+ stats.patternsPromoted = promotionResult.promoted ?? 0;
459
+ }
460
+ }
461
+ console.log('[QEHooks] Pre-compaction flush:', stats);
462
+ return {
463
+ success: true,
464
+ data: stats,
465
+ };
466
+ },
437
467
  };
438
468
  }
439
469
  // ============================================================================
@@ -195,6 +195,12 @@ export declare function calculateQualityScore(pattern: {
195
195
  /**
196
196
  * Pattern promotion check result
197
197
  */
198
+ /**
199
+ * Shared promotion threshold: minimum successful uses before a short-term
200
+ * pattern can be promoted to long-term. Used by pattern-store, pattern-lifecycle,
201
+ * experience-capture, and shouldPromotePattern().
202
+ */
203
+ export declare const PROMOTION_THRESHOLD = 3;
198
204
  export interface PromotionCheck {
199
205
  meetsUsageCriteria: boolean;
200
206
  meetsQualityCriteria: boolean;
@@ -72,6 +72,15 @@ export function calculateQualityScore(pattern) {
72
72
  const usageScore = Math.min(pattern.usageCount / 100, 1);
73
73
  return (pattern.confidence * 0.3 + usageScore * 0.2 + pattern.successRate * 0.5);
74
74
  }
75
+ /**
76
+ * Pattern promotion check result
77
+ */
78
+ /**
79
+ * Shared promotion threshold: minimum successful uses before a short-term
80
+ * pattern can be promoted to long-term. Used by pattern-store, pattern-lifecycle,
81
+ * experience-capture, and shouldPromotePattern().
82
+ */
83
+ export const PROMOTION_THRESHOLD = 3;
75
84
  /**
76
85
  * Check if pattern should be promoted to long-term storage
77
86
  * Requires 3+ successful uses as per ADR-021
@@ -82,7 +91,7 @@ export function calculateQualityScore(pattern) {
82
91
  * @param coherenceThreshold - Threshold for coherence violation (default: 0.4)
83
92
  */
84
93
  export function shouldPromotePattern(pattern, coherenceEnergy, coherenceThreshold = 0.4) {
85
- const meetsUsageCriteria = pattern.tier === 'short-term' && pattern.successfulUses >= 3;
94
+ const meetsUsageCriteria = pattern.tier === 'short-term' && pattern.successfulUses >= PROMOTION_THRESHOLD;
86
95
  const meetsQualityCriteria = pattern.successRate >= 0.7 && pattern.confidence >= 0.6;
87
96
  // NEW: Coherence criteria - only block if coherence energy is provided and exceeds threshold
88
97
  const meetsCoherenceCriteria = coherenceEnergy === undefined || coherenceEnergy < coherenceThreshold;