@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.
- package/dist/lib/__tests__/connection-manager.test.js +2 -2
- package/dist/lib/__tests__/connection-manager.test.js.map +1 -1
- package/dist/lib/__tests__/qwickbrain-client.test.js +3 -1
- package/dist/lib/__tests__/qwickbrain-client.test.js.map +1 -1
- package/dist/lib/connection-manager.d.ts +7 -0
- package/dist/lib/connection-manager.d.ts.map +1 -1
- package/dist/lib/connection-manager.js +57 -8
- package/dist/lib/connection-manager.js.map +1 -1
- package/dist/lib/proxy-server.d.ts +7 -0
- package/dist/lib/proxy-server.d.ts.map +1 -1
- package/dist/lib/proxy-server.js +41 -10
- package/dist/lib/proxy-server.js.map +1 -1
- package/dist/lib/qwickbrain-client.d.ts.map +1 -1
- package/dist/lib/qwickbrain-client.js +26 -16
- package/dist/lib/qwickbrain-client.js.map +1 -1
- package/dist/lib/sse-invalidation-listener.d.ts +5 -1
- package/dist/lib/sse-invalidation-listener.d.ts.map +1 -1
- package/dist/lib/sse-invalidation-listener.js +24 -18
- package/dist/lib/sse-invalidation-listener.js.map +1 -1
- package/dist/lib/tools.d.ts.map +1 -1
- package/dist/lib/tools.js +29 -4
- package/dist/lib/tools.js.map +1 -1
- package/package.json +1 -1
- package/src/lib/__tests__/connection-manager.test.ts +2 -2
- package/src/lib/__tests__/qwickbrain-client.test.ts +3 -1
- package/src/lib/connection-manager.ts +67 -8
- package/src/lib/proxy-server.ts +49 -12
- package/src/lib/qwickbrain-client.ts +29 -15
- package/src/lib/sse-invalidation-listener.ts +32 -18
- package/src/lib/tools.ts +29 -4
- package/.claude/engineering/bugs/BUG-qwickbrain-proxy-cache-and-design.md +0 -840
- 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)
|