@qwickapps/qwickbrain-proxy 1.0.3 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/dist/lib/__tests__/connection-manager.test.js +2 -2
  2. package/dist/lib/__tests__/connection-manager.test.js.map +1 -1
  3. package/dist/lib/__tests__/qwickbrain-client.test.js +3 -1
  4. package/dist/lib/__tests__/qwickbrain-client.test.js.map +1 -1
  5. package/dist/lib/connection-manager.d.ts +7 -0
  6. package/dist/lib/connection-manager.d.ts.map +1 -1
  7. package/dist/lib/connection-manager.js +57 -8
  8. package/dist/lib/connection-manager.js.map +1 -1
  9. package/dist/lib/proxy-server.d.ts +7 -0
  10. package/dist/lib/proxy-server.d.ts.map +1 -1
  11. package/dist/lib/proxy-server.js +41 -10
  12. package/dist/lib/proxy-server.js.map +1 -1
  13. package/dist/lib/qwickbrain-client.d.ts.map +1 -1
  14. package/dist/lib/qwickbrain-client.js +26 -16
  15. package/dist/lib/qwickbrain-client.js.map +1 -1
  16. package/dist/lib/sse-invalidation-listener.d.ts +5 -1
  17. package/dist/lib/sse-invalidation-listener.d.ts.map +1 -1
  18. package/dist/lib/sse-invalidation-listener.js +24 -18
  19. package/dist/lib/sse-invalidation-listener.js.map +1 -1
  20. package/dist/lib/tools.d.ts.map +1 -1
  21. package/dist/lib/tools.js +29 -4
  22. package/dist/lib/tools.js.map +1 -1
  23. package/package.json +1 -1
  24. package/src/lib/__tests__/connection-manager.test.ts +2 -2
  25. package/src/lib/__tests__/qwickbrain-client.test.ts +3 -1
  26. package/src/lib/connection-manager.ts +67 -8
  27. package/src/lib/proxy-server.ts +49 -12
  28. package/src/lib/qwickbrain-client.ts +29 -15
  29. package/src/lib/sse-invalidation-listener.ts +32 -18
  30. package/src/lib/tools.ts +29 -4
  31. package/.claude/engineering/bugs/BUG-qwickbrain-proxy-cache-and-design.md +0 -840
  32. package/.github/workflows/publish.yml +0 -105
@@ -1,840 +0,0 @@
1
- # Bug Analysis: QwickBrain Proxy Cache and Design Issues
2
-
3
- ## Bug ID
4
-
5
- qwickbrain-proxy-cache-and-design
6
-
7
- ## Date
8
-
9
- 2026-01-09
10
-
11
- ## Reporter
12
-
13
- User testing
14
-
15
- ## Summary
16
-
17
- QwickBrain proxy fails to deliver requested features. Returns empty content for get_memory calls and exposes only 3 fallback tools instead of all available tools.
18
-
19
- ## Test Output
20
-
21
- ```json
22
- get_memory(name="qwickbrain-context", project="qwickbrain")
23
- {
24
- "data": {
25
- "name": "qwickbrain-context",
26
- "project": "qwickbrain",
27
- "content": "", // ← EMPTY CONTENT
28
- "metadata": {}
29
- },
30
- "_metadata": {
31
- "source": "cache", // ← Serving from cache
32
- "age_seconds": 315, // ← Cache is 5 minutes old
33
- "status": "disconnected" // ← Not connected when request made
34
- }
35
- }
36
- ```
37
-
38
- Available tools: `get_workflow, get_document, get_memory` (only 3 instead of 10+)
39
-
40
- ## Root Causes
41
-
42
- ### Issue 1: Background Cache Sync Not Implemented
43
-
44
- **Location:** `src/lib/proxy-server.ts:71-91`
45
-
46
- ```typescript
47
- private async onConnectionRestored(): Promise<void> {
48
- console.error('Starting background cache sync...');
49
-
50
- const preloadItems = this.config.cache.preload || [];
51
- for (const itemType of preloadItems) {
52
- try {
53
- if (itemType === 'workflows') {
54
- // TODO: List and cache all workflows
55
- console.error('Preloading workflows...');
56
- } else if (itemType === 'rules') {
57
- // TODO: List and cache all rules
58
- console.error('Preloading rules...');
59
- }
60
- } catch (error) {
61
- console.error(`Failed to preload ${itemType}:`, error);
62
- }
63
- }
64
-
65
- console.error('Background cache sync complete');
66
- }
67
- ```
68
-
69
- **Evidence:** Method has TODO comments, doesn't fetch or cache anything.
70
-
71
- **Impact:**
72
-
73
- - Cache remains empty unless explicitly populated by client requests
74
- - Static content (rules, agents, templates) never pre-cached
75
- - Offline mode is useless - no cached data to serve
76
-
77
- ### Issue 2: Dynamic Tool Listing (Non-Standard Design)
78
-
79
- **Location:** `src/lib/proxy-server.ts:94-148`
80
-
81
- ```typescript
82
- this.server.setRequestHandler(ListToolsRequestSchema, async () => {
83
- // Try to fetch tools from upstream if connected
84
- if (this.connectionManager.getState() === 'connected') {
85
- try {
86
- const tools = await this.connectionManager.execute(async () => {
87
- return await this.qwickbrainClient.listTools();
88
- });
89
- return { tools };
90
- } catch (error) {
91
- console.error('Failed to list tools from upstream:', error);
92
- }
93
- }
94
-
95
- // Fallback to minimal tool set when offline or error
96
- return {
97
- tools: [
98
- { name: 'get_workflow', ...},
99
- { name: 'get_document', ...},
100
- { name: 'get_memory', ...}
101
- ],
102
- };
103
- });
104
- ```
105
-
106
- **Evidence:** Tools queried dynamically from upstream when connected, falls back to 3 tools when offline.
107
-
108
- **Impact:**
109
-
110
- - Non-standard MCP behavior (tool list should be static)
111
- - Race condition: client connects before proxy connects to upstream → sees 3 tools
112
- - Inconsistent tool availability based on connection state
113
-
114
- ### Issue 3: No Write-Through Cache for Static Content
115
-
116
- **Location:** `src/lib/cache-manager.ts`, `src/lib/proxy-server.ts:224-289`
117
-
118
- Current behavior:
119
-
120
- 1. Client requests document/memory
121
- 2. Check cache (if not expired, return)
122
- 3. Try upstream (if connected, cache and return)
123
- 4. Try stale cache (if exists, return)
124
- 5. Return error (no cache, no connection)
125
-
126
- **Missing:**
127
-
128
- - No differentiation between static (rules, agents, templates) and dynamic content
129
- - No write-through caching - static content should be cached on every fetch
130
- - No permanent cache flag - static content TTL should be very long or infinite
131
-
132
- **Impact:**
133
-
134
- - Static content not reliably available offline
135
- - Frequent re-fetching of rarely-changing content
136
- - No guarantee critical content is cached
137
-
138
- ### Issue 4: QwickBrain Python Server Missing Tools
139
-
140
- **Location:** `/Users/raajkumars/Projects/qwickbrain/src/qwickbrain/mcp/server.py:212-359`
141
-
142
- The Python MCP server implements 10+ tools:
143
-
144
- - get_workflow
145
- - get_document
146
- - get_memory
147
- - set_memory
148
- - list_repositories
149
- - search_codebase
150
- - find_functions
151
- - find_classes
152
- - explain_function
153
- - analyze_file
154
- - analyze_repository
155
- - update_workflow
156
-
157
- But proxy only exposes 3 when offline.
158
-
159
- ## Design Requirements (from user)
160
-
161
- ### Requirement 1: Static Tool Mapping
162
-
163
- - All functions should be permanently mapped in proxy
164
- - Tools don't need dynamic discovery
165
- - Functions return "offline" error when QwickBrain unreachable
166
- - Consistent tool availability regardless of connection state
167
-
168
- ### Requirement 2: Storage-Limited Cache with LRU Eviction (NO TTL)
169
-
170
- **Rationale:** User works for 8+ hours on corporate network - TTL would cause unnecessary cache invalidation during active work sessions.
171
-
172
- **Cache Strategy:**
173
-
174
- - **No TTL-based expiration** - cache stays valid indefinitely
175
- - **Two-tier storage:**
176
- - **Critical tier (permanent)**: Never evicted, not counted toward storage limit
177
- - Rules, agents, templates, workflows
178
- - Always available offline
179
- - **Dynamic tier (LRU)**: Storage-limited with LRU eviction
180
- - Documents (FRDs, designs, ADRs)
181
- - Memories (context, sprint handoffs)
182
- - Configurable max size (e.g., 100MB, 500MB)
183
- - **LRU eviction** - when dynamic tier storage limit reached, evict least recently used entries
184
- - **SSE-based invalidation** - cache invalidated when QwickBrain sends update notifications
185
-
186
- ### Requirement 3: Offline Write Queue with Auto-Sync
187
-
188
- - **Write-through when online** - writes go directly to QwickBrain and cache
189
- - **Queue when offline** - writes stored in local queue
190
- - **Auto-sync on reconnection** - queued writes automatically sent when connection restored
191
- - **Conflict detection** (future) - for multi-user scenarios
192
-
193
- ### Requirement 4: Real-Time Cache Invalidation via SSE
194
-
195
- - QwickBrain sends SSE notifications when documents/memories updated
196
- - Proxy listens to SSE stream for update events
197
- - Cache entry invalidated on update notification
198
- - Next access fetches fresh data from QwickBrain
199
- - **Scope:** Single user for now, multi-tenancy support later
200
-
201
- ### Requirement 5: Background Cache Preload
202
-
203
- - On connection, preload critical static content:
204
- - All workflows
205
- - All rules
206
- - All agents
207
- - All templates
208
- - Preload happens in background, doesn't block proxy startup
209
- - Subsequent access serves from cache (no repeated fetches)
210
-
211
- ## Proposed Fix
212
-
213
- ### Fix 1: Implement Static Tool Mapping
214
-
215
- **File:** `src/lib/proxy-server.ts:94-148`
216
-
217
- Replace dynamic tool listing with static tool map:
218
-
219
- ```typescript
220
- private getAllTools() {
221
- return [
222
- { name: 'get_workflow', description: 'Get a workflow definition by name', inputSchema: { /* ... */ } },
223
- { name: 'get_document', description: 'Get a document by name and type', inputSchema: { /* ... */ } },
224
- { name: 'get_memory', description: 'Get a memory/context document', inputSchema: { /* ... */ } },
225
- { name: 'set_memory', description: 'Set or update a memory/context document', inputSchema: { /* ... */ } },
226
- { name: 'update_document', description: 'Update a document', inputSchema: { /* ... */ } },
227
- { name: 'list_repositories', description: 'List all indexed repositories', inputSchema: { /* ... */ } },
228
- { name: 'search_codebase', description: 'Search code across all repositories', inputSchema: { /* ... */ } },
229
- { name: 'find_functions', description: 'Find function definitions', inputSchema: { /* ... */ } },
230
- { name: 'find_classes', description: 'Find class definitions', inputSchema: { /* ... */ } },
231
- { name: 'explain_function', description: 'Explain function implementation', inputSchema: { /* ... */ } },
232
- { name: 'analyze_file', description: 'Analyze file structure', inputSchema: { /* ... */ } },
233
- { name: 'analyze_repository', description: 'Analyze repository structure', inputSchema: { /* ... */ } },
234
- ];
235
- }
236
-
237
- this.server.setRequestHandler(ListToolsRequestSchema, async () => {
238
- return { tools: this.getAllTools() };
239
- });
240
- ```
241
-
242
- Tools always available, return "offline" error in handler when disconnected.
243
-
244
- ### Fix 2: Remove TTL, Implement LRU Cache with Storage Limit
245
-
246
- **File:** `src/lib/cache-manager.ts`, `src/db/schema.ts`
247
-
248
- **Two-tier storage schema:**
249
-
250
- ```typescript
251
- // Remove expiresAt column from documents and memories tables
252
- // Add isCritical flag to separate critical from dynamic tier
253
- // Add lastAccessedAt column for LRU tracking (dynamic tier only)
254
-
255
- export const documents = sqliteTable('documents', {
256
- // ... existing columns ...
257
- cachedAt: integer('cached_at', { mode: 'timestamp' }).notNull(),
258
- lastAccessedAt: integer('last_accessed_at', { mode: 'timestamp' }).notNull(),
259
- isCritical: integer('is_critical', { mode: 'boolean' }).notNull().default(false),
260
- sizeBytes: integer('size_bytes').notNull(),
261
- });
262
-
263
- // Critical doc types: workflow, rule, agent, template
264
- const CRITICAL_DOC_TYPES = ['workflow', 'rule', 'agent', 'template'];
265
- ```
266
-
267
- **Implement two-tier LRU eviction:**
268
-
269
- ```typescript
270
- class CacheManager {
271
- private maxDynamicCacheSize: number; // Configurable, e.g., 100MB (only for dynamic tier)
272
-
273
- async ensureCacheSize(requiredBytes: number, isCritical: boolean): Promise<void> {
274
- // Critical files bypass storage limit check
275
- if (isCritical) {
276
- return;
277
- }
278
-
279
- // Only count dynamic tier (isCritical=false) toward storage limit
280
- const currentSize = await this.getDynamicCacheSize();
281
- if (currentSize + requiredBytes <= this.maxDynamicCacheSize) {
282
- return;
283
- }
284
-
285
- // Evict LRU entries from dynamic tier only
286
- const toEvict = currentSize + requiredBytes - this.maxDynamicCacheSize;
287
- await this.evictLRU(toEvict);
288
- }
289
-
290
- private async getDynamicCacheSize(): Promise<number> {
291
- // Only count non-critical items
292
- const result = await this.db
293
- .select({ total: sql<number>`sum(${documents.sizeBytes})` })
294
- .from(documents)
295
- .where(eq(documents.isCritical, false));
296
- return result[0]?.total || 0;
297
- }
298
-
299
- private async evictLRU(bytesToFree: number): Promise<void> {
300
- // Only evict from dynamic tier (isCritical=false)
301
- // NEVER touch critical tier
302
- let freed = 0;
303
- const candidates = await this.db
304
- .select()
305
- .from(documents)
306
- .where(eq(documents.isCritical, false))
307
- .orderBy(documents.lastAccessedAt) // ASC = oldest first
308
- .limit(100);
309
-
310
- for (const doc of candidates) {
311
- await this.db.delete(documents).where(eq(documents.id, doc.id));
312
- freed += doc.sizeBytes;
313
- console.error(`Evicted: ${doc.docType}:${doc.name} (${doc.sizeBytes} bytes)`);
314
- if (freed >= bytesToFree) break;
315
- }
316
- }
317
-
318
- async setDocument(
319
- docType: string,
320
- name: string,
321
- content: string,
322
- project?: string,
323
- metadata?: Record<string, unknown>
324
- ): Promise<void> {
325
- const isCritical = CRITICAL_DOC_TYPES.includes(docType);
326
- const sizeBytes = Buffer.byteLength(content, 'utf8');
327
-
328
- // Ensure space available (skips check if critical)
329
- await this.ensureCacheSize(sizeBytes, isCritical);
330
-
331
- // Insert/update with critical flag
332
- await this.db.insert(documents).values({
333
- docType,
334
- name,
335
- project: project || '',
336
- content,
337
- metadata: JSON.stringify(metadata || {}),
338
- cachedAt: new Date(),
339
- lastAccessedAt: new Date(),
340
- isCritical,
341
- sizeBytes,
342
- }).onConflictDoUpdate({
343
- target: [documents.docType, documents.name, documents.project],
344
- set: {
345
- content,
346
- metadata: JSON.stringify(metadata || {}),
347
- lastAccessedAt: new Date(),
348
- sizeBytes,
349
- },
350
- });
351
- }
352
-
353
- async getDocument(...): Promise<CachedItem<any> | null> {
354
- const cached = await this.db.select()...;
355
- if (!cached) return null;
356
-
357
- // Update last accessed timestamp (for LRU tracking)
358
- await this.db.update(documents)
359
- .set({ lastAccessedAt: new Date() })
360
- .where(eq(documents.id, cached.id));
361
-
362
- return { data: cached.data, age: ... };
363
- }
364
- }
365
- ```
366
-
367
- ### Fix 3: Implement Offline Write Queue
368
-
369
- **File:** `src/lib/write-queue.ts` (new), `src/db/schema.ts`
370
-
371
- **Add pending_writes table:**
372
-
373
- ```typescript
374
- export const pendingWrites = sqliteTable('pending_writes', {
375
- id: integer('id').primaryKey(),
376
- operation: text('operation').notNull(), // 'set_memory', 'update_document', etc.
377
- docType: text('doc_type'),
378
- name: text('name').notNull(),
379
- project: text('project').notNull().default(''),
380
- content: text('content').notNull(),
381
- metadata: text('metadata'),
382
- queuedAt: integer('queued_at', { mode: 'timestamp' }).notNull(),
383
- retries: integer('retries').notNull().default(0),
384
- });
385
- ```
386
-
387
- **Implement WriteQueue:**
388
-
389
- ```typescript
390
- class WriteQueue {
391
- async queueWrite(operation: string, args: any): Promise<void> {
392
- await this.db.insert(pendingWrites).values({
393
- operation,
394
- docType: args.doc_type,
395
- name: args.name,
396
- project: args.project || '',
397
- content: args.content,
398
- metadata: JSON.stringify(args.metadata || {}),
399
- queuedAt: new Date(),
400
- });
401
- }
402
-
403
- async syncPendingWrites(): Promise<void> {
404
- const pending = await this.db.select().from(pendingWrites);
405
-
406
- for (const write of pending) {
407
- try {
408
- // Execute write against QwickBrain
409
- await this.qwickbrainClient[write.operation](JSON.parse(write));
410
- // Remove from queue on success
411
- await this.db.delete(pendingWrites).where(eq(pendingWrites.id, write.id));
412
- } catch (error) {
413
- // Increment retry count
414
- await this.db.update(pendingWrites)
415
- .set({ retries: write.retries + 1 })
416
- .where(eq(pendingWrites.id, write.id));
417
- }
418
- }
419
- }
420
- }
421
- ```
422
-
423
- **Integrate with proxy:**
424
-
425
- ```typescript
426
- // In handleSetMemory, handleUpdateDocument, etc.
427
- if (this.connectionManager.getState() === 'connected') {
428
- // Write-through
429
- await this.qwickbrainClient.setMemory(...);
430
- await this.cacheManager.setMemory(...);
431
- } else {
432
- // Queue for later
433
- await this.writeQueue.queueWrite('setMemory', args);
434
- await this.cacheManager.setMemory(...); // Update local cache optimistically
435
- }
436
-
437
- // On connection restored:
438
- await this.writeQueue.syncPendingWrites();
439
- ```
440
-
441
- ### Fix 4: Implement SSE-Based Cache Invalidation
442
-
443
- **File:** `src/lib/cache-invalidator.ts` (new)
444
-
445
- **QwickBrain SSE endpoint:** `GET /sse/updates`
446
- Sends events like:
447
-
448
- ```json
449
- {
450
- "type": "document_updated",
451
- "doc_type": "rule",
452
- "name": "WRITING-STYLE",
453
- "project": "",
454
- "updated_by": "user123",
455
- "timestamp": "2026-01-09T18:30:00Z"
456
- }
457
- ```
458
-
459
- **Implement CacheInvalidator:**
460
-
461
- ```typescript
462
- class CacheInvalidator {
463
- private eventSource: EventSource | null = null;
464
-
465
- async start(): Promise<void> {
466
- if (!this.config.url) return;
467
-
468
- this.eventSource = new EventSource(`${this.config.url}/sse/updates`);
469
-
470
- this.eventSource.addEventListener('document_updated', async (event) => {
471
- const data = JSON.parse(event.data);
472
- await this.cacheManager.invalidateDocument(data.doc_type, data.name, data.project);
473
- console.error(`Cache invalidated: ${data.doc_type}:${data.name}`);
474
- });
475
-
476
- this.eventSource.addEventListener('memory_updated', async (event) => {
477
- const data = JSON.parse(event.data);
478
- await this.cacheManager.invalidateMemory(data.name, data.project);
479
- console.error(`Cache invalidated: memory:${data.name}`);
480
- });
481
-
482
- this.eventSource.onerror = () => {
483
- console.error('SSE connection error, will reconnect...');
484
- };
485
- }
486
-
487
- stop(): void {
488
- this.eventSource?.close();
489
- this.eventSource = null;
490
- }
491
- }
492
- ```
493
-
494
- **Add invalidation methods to CacheManager:**
495
-
496
- ```typescript
497
- async invalidateDocument(docType: string, name: string, project?: string): Promise<void> {
498
- await this.db.delete(documents).where(
499
- and(
500
- eq(documents.docType, docType),
501
- eq(documents.name, name),
502
- eq(documents.project, project || '')
503
- )
504
- );
505
- }
506
-
507
- async invalidateMemory(name: string, project?: string): Promise<void> {
508
- await this.db.delete(memories).where(
509
- and(
510
- eq(memories.name, name),
511
- eq(memories.project, project || '')
512
- )
513
- );
514
- }
515
- ```
516
-
517
- ### Fix 5: Implement Background Cache Preload
518
-
519
- **File:** `src/lib/proxy-server.ts:71-91`
520
-
521
- ```typescript
522
- private async onConnectionRestored(): Promise<void> {
523
- console.error('Starting background cache sync...');
524
-
525
- try {
526
- // Preload all workflows (critical priority)
527
- const workflows = await this.qwickbrainClient.listDocuments('workflow');
528
- for (const wf of workflows) {
529
- const content = await this.qwickbrainClient.getDocument('workflow', wf.name);
530
- await this.cacheManager.setDocument('workflow', wf.name, content.content, undefined, undefined, 2); // priority=2
531
- }
532
-
533
- // Preload all rules (critical priority)
534
- const rules = await this.qwickbrainClient.listDocuments('rule');
535
- for (const rule of rules) {
536
- const content = await this.qwickbrainClient.getDocument('rule', rule.name);
537
- await this.cacheManager.setDocument('rule', rule.name, content.content, undefined, undefined, 2);
538
- }
539
-
540
- // Preload agents, templates (critical priority)
541
- // ...
542
-
543
- // Start SSE listener for real-time invalidation
544
- await this.cacheInvalidator.start();
545
-
546
- // Sync any pending writes
547
- await this.writeQueue.syncPendingWrites();
548
-
549
- } catch (error) {
550
- console.error('Background sync error:', error);
551
- }
552
-
553
- console.error('Background cache sync complete');
554
- }
555
- ```
556
-
557
- ### Fix 6: Return "Offline" Error for Non-Cached Tools
558
-
559
- **File:** `src/lib/proxy-server.ts:174-177`
560
-
561
- ```typescript
562
- default:
563
- // Generic forwarding for non-cacheable tools
564
- if (this.connectionManager.getState() !== 'connected') {
565
- throw new Error('QwickBrain offline - this tool requires active connection');
566
- }
567
- result = await this.handleGenericTool(name, args || {});
568
- break;
569
- ```
570
-
571
- ## Files to Modify
572
-
573
- ### Phase 1: Static Tool Mapping (Quick Win)
574
-
575
- 1. `src/lib/proxy-server.ts`:
576
- - Replace dynamic tool listing with static map (lines 94-148)
577
- - Add offline error for non-cacheable tools (lines 174-177)
578
-
579
- ### Phase 2: LRU Cache Implementation
580
-
581
- 2. `src/db/schema.ts`:
582
- - Remove `expiresAt` column
583
- - Add `lastAccessedAt`, `priority`, `sizeBytes` columns
584
- - Create database migration
585
-
586
- 2. `src/lib/cache-manager.ts`:
587
- - Remove TTL-based expiration logic
588
- - Implement LRU eviction with storage limit
589
- - Update `lastAccessedAt` on every read
590
- - Add `invalidateDocument()`, `invalidateMemory()` methods
591
- - Respect priority tiers during eviction
592
-
593
- 3. `src/types/config.ts`:
594
- - Add `maxCacheSizeBytes` to cache configuration
595
- - Remove TTL configuration
596
-
597
- ### Phase 3: Offline Write Queue
598
-
599
- 5. `src/db/schema.ts`:
600
- - Add `pending_writes` table
601
-
602
- 2. `src/lib/write-queue.ts` (new):
603
- - Implement `WriteQueue` class
604
- - `queueWrite()` - add to pending writes
605
- - `syncPendingWrites()` - sync on reconnection
606
-
607
- 3. `src/lib/proxy-server.ts`:
608
- - Integrate write queue into `set_memory`, `update_document` handlers
609
- - Call `syncPendingWrites()` in `onConnectionRestored()`
610
-
611
- 4. `src/lib/qwickbrain-client.ts`:
612
- - Add `setMemory()`, `updateDocument()` methods
613
-
614
- ### Phase 4: SSE-Based Cache Invalidation
615
-
616
- 9. `src/lib/cache-invalidator.ts` (new):
617
- - Implement `CacheInvalidator` class
618
- - Listen to SSE `/sse/updates` endpoint
619
- - Call `cacheManager.invalidate*()` on update events
620
-
621
- 2. `src/lib/proxy-server.ts`:
622
- - Initialize `CacheInvalidator`
623
- - Start SSE listener in `onConnectionRestored()`
624
- - Stop listener on disconnect
625
-
626
- 3. **QwickBrain Python server** (separate repo):
627
- - Add SSE `/sse/updates` endpoint
628
- - Emit `document_updated`, `memory_updated` events
629
-
630
- ### Phase 5: Background Cache Preload
631
-
632
- 12. `src/lib/proxy-server.ts`:
633
- - Implement background cache sync (lines 71-91)
634
- - Preload workflows, rules, agents, templates with priority=2
635
-
636
- 2. `src/lib/qwickbrain-client.ts`:
637
- - Add `listDocuments(docType)` method
638
-
639
- ## Testing Plan
640
-
641
- ### Test 1: Static Tool Availability (Phase 1)
642
-
643
- ```typescript
644
- // Start proxy
645
- // Immediately call listTools() before connection established
646
- // Verify ALL 12+ tools are exposed (not just 3)
647
- assert(tools.length >= 12);
648
- assert(tools.find(t => t.name === 'search_codebase'));
649
- ```
650
-
651
- ### Test 2: LRU Eviction with Two-Tier Storage (Phase 2)
652
-
653
- ```typescript
654
- // Configure dynamic tier cache limit: 10MB
655
- // Preload critical files (workflows, rules, agents, templates): 15MB
656
- // Preload dynamic documents: 50MB
657
- // Verify:
658
- // - Critical files: 15MB stored, not counted toward 10MB limit
659
- // - Dynamic files: Only 10MB stored (40MB evicted via LRU)
660
- // - Total cache size: 25MB (15MB critical + 10MB dynamic)
661
- // - All critical files present in cache
662
- // - Only most recently accessed dynamic files present
663
- // - Least recently accessed dynamic files evicted
664
- // Access a critical file (workflow)
665
- // Verify it's never evicted even with more dynamic file additions
666
- ```
667
-
668
- ### Test 3: Offline Write Queue (Phase 3)
669
-
670
- ```typescript
671
- // Start proxy (online)
672
- // Stop QwickBrain (simulate offline)
673
- // Call set_memory('test-context', 'content')
674
- // Verify queued in pending_writes table
675
- // Restart QwickBrain
676
- // Wait for auto-sync
677
- // Verify write sent to QwickBrain
678
- // Verify removed from pending_writes table
679
- ```
680
-
681
- ### Test 4: SSE Cache Invalidation (Phase 4)
682
-
683
- ```typescript
684
- // Start proxy, cache document 'WRITING-STYLE'
685
- // Simulate SSE event: { type: 'document_updated', doc_type: 'rule', name: 'WRITING-STYLE' }
686
- // Verify cache entry deleted
687
- // Call get_document('rule', 'WRITING-STYLE')
688
- // Verify fresh fetch from QwickBrain (not cache)
689
- ```
690
-
691
- ### Test 5: Background Preload (Phase 5)
692
-
693
- ```typescript
694
- // Start proxy, wait for background sync
695
- // Stop QwickBrain (offline mode)
696
- // Call get_document('workflow', 'feature')
697
- // Verify content returned from cache (not empty)
698
- // Call get_document('rule', 'WRITING-STYLE')
699
- // Verify content returned from cache (not empty)
700
- ```
701
-
702
- ### Test 6: Offline Mode with Cached Content
703
-
704
- ```typescript
705
- // Start proxy, wait for background sync
706
- // Stop QwickBrain server
707
- // Call get_document('rule', 'WRITING-STYLE')
708
- // Verify content returned from cache, not empty
709
- // Call search_codebase(...) - non-cacheable tool
710
- // Verify returns "QwickBrain offline" error
711
- ```
712
-
713
- ### Test 7: 8-Hour Work Session (No TTL Invalidation)
714
-
715
- ```typescript
716
- // Start proxy at 9am
717
- // Access documents throughout day
718
- // At 5pm (8 hours later):
719
- // Verify all accessed documents still in cache
720
- // Verify no TTL-based expiration occurred
721
- // Verify LRU eviction only triggered by storage limit
722
- ```
723
-
724
- ## Success Criteria
725
-
726
- ### Phase 1 (Static Tool Mapping)
727
-
728
- - [ ] All 12+ tools exposed in listTools() regardless of connection state
729
- - [ ] Non-cacheable tools return "QwickBrain offline" error when disconnected
730
-
731
- ### Phase 2 (LRU Cache)
732
-
733
- - [ ] No TTL-based expiration - cache entries stay valid indefinitely
734
- - [ ] Two-tier storage:
735
- - [ ] Critical tier (workflows, rules, agents, templates) - never evicted, not counted toward limit
736
- - [ ] Dynamic tier - storage-limited with configurable size (default 100MB)
737
- - [ ] LRU eviction only affects dynamic tier
738
- - [ ] Critical files always available offline
739
- - [ ] 8-hour work session: no cache invalidation, all content available
740
-
741
- ### Phase 3 (Offline Write Queue)
742
-
743
- - [ ] Writes queued when offline
744
- - [ ] Auto-sync on reconnection
745
- - [ ] Queued writes successfully sent to QwickBrain
746
- - [ ] Optimistic local cache updates work offline
747
-
748
- ### Phase 4 (SSE Cache Invalidation)
749
-
750
- - [ ] SSE listener active when connected
751
- - [ ] Cache invalidated on update notifications
752
- - [ ] Fresh fetch after invalidation
753
- - [ ] No stale data served after remote updates
754
-
755
- ### Phase 5 (Background Preload)
756
-
757
- - [ ] Static content (workflows, rules, agents, templates) preloaded on connection
758
- - [ ] Offline mode serves preloaded content successfully
759
- - [ ] Background sync doesn't block proxy startup
760
-
761
- ### Overall
762
-
763
- - [ ] Proxy works offline with preloaded cache
764
- - [ ] No empty content returned for cached documents
765
- - [ ] Writes work offline and sync automatically
766
- - [ ] Cache stays valid during 8-hour work sessions
767
- - [ ] Real-time updates invalidate cache appropriately
768
- - [ ] All 7 tests pass
769
-
770
- ## Implementation Phases
771
-
772
- ### Phase 1: Static Tool Mapping (1-2 hours)
773
-
774
- Quick win - fixes immediate tool availability issue.
775
-
776
- **Deliverables:**
777
-
778
- - All tools exposed statically
779
- - Offline error for non-cached tools
780
- - Test 1 passes
781
-
782
- ### Phase 2: LRU Cache (4-6 hours)
783
-
784
- Remove TTL, implement storage-based eviction.
785
-
786
- **Deliverables:**
787
-
788
- - Database migration
789
- - LRU eviction logic
790
- - Storage tracking
791
- - Priority tiers
792
- - Tests 2, 7 pass
793
-
794
- ### Phase 3: Offline Write Queue (3-4 hours)
795
-
796
- Enable offline writes with auto-sync.
797
-
798
- **Deliverables:**
799
-
800
- - Write queue table
801
- - Queue/sync logic
802
- - Integration with handlers
803
- - Test 3 passes
804
-
805
- ### Phase 4: SSE Cache Invalidation (3-4 hours)
806
-
807
- Real-time cache invalidation.
808
-
809
- **Deliverables:**
810
-
811
- - CacheInvalidator class
812
- - SSE listener
813
- - Invalidation logic
814
- - Test 4 passes
815
- - **Requires:** QwickBrain Python server SSE endpoint (separate task)
816
-
817
- ### Phase 5: Background Preload (2-3 hours)
818
-
819
- Preload critical static content.
820
-
821
- **Deliverables:**
822
-
823
- - Background sync implementation
824
- - listDocuments() method
825
- - Priority-based preloading
826
- - Tests 5, 6 pass
827
-
828
- **Total Estimated Effort:** 13-19 hours
829
-
830
- ## Next Steps
831
-
832
- 1. **User approval** - Confirm phased approach
833
- 2. **Start with Phase 1** - Quick win, static tool mapping
834
- 3. **Test Phase 1** - Verify tool availability
835
- 4. **Continue with Phase 2** - LRU cache
836
- 5. **QwickBrain SSE endpoint** - Coordinate with Python server implementation
837
- 6. **Phases 3-5** - Complete remaining features
838
- 7. **Integration testing** - All phases together
839
- 8. **Documentation** - Update README, architecture docs
840
- 9. **Version bump and release** - v1.1.0 (breaking changes to cache schema)