opencode-swarm-plugin 0.42.9 → 0.44.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 (48) hide show
  1. package/.hive/issues.jsonl +14 -0
  2. package/.turbo/turbo-build.log +2 -2
  3. package/CHANGELOG.md +110 -0
  4. package/README.md +296 -6
  5. package/bin/cass.characterization.test.ts +422 -0
  6. package/bin/swarm.test.ts +683 -0
  7. package/bin/swarm.ts +501 -0
  8. package/dist/contributor-tools.d.ts +42 -0
  9. package/dist/contributor-tools.d.ts.map +1 -0
  10. package/dist/dashboard.d.ts +83 -0
  11. package/dist/dashboard.d.ts.map +1 -0
  12. package/dist/error-enrichment.d.ts +49 -0
  13. package/dist/error-enrichment.d.ts.map +1 -0
  14. package/dist/export-tools.d.ts +76 -0
  15. package/dist/export-tools.d.ts.map +1 -0
  16. package/dist/index.d.ts +14 -2
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +95 -2
  19. package/dist/observability-tools.d.ts +2 -2
  20. package/dist/plugin.js +95 -2
  21. package/dist/query-tools.d.ts +59 -0
  22. package/dist/query-tools.d.ts.map +1 -0
  23. package/dist/replay-tools.d.ts +28 -0
  24. package/dist/replay-tools.d.ts.map +1 -0
  25. package/dist/sessions/agent-discovery.d.ts +59 -0
  26. package/dist/sessions/agent-discovery.d.ts.map +1 -0
  27. package/dist/sessions/index.d.ts +10 -0
  28. package/dist/sessions/index.d.ts.map +1 -0
  29. package/docs/planning/ADR-010-cass-inhousing.md +1215 -0
  30. package/evals/fixtures/cass-baseline.ts +217 -0
  31. package/examples/plugin-wrapper-template.ts +89 -0
  32. package/package.json +1 -1
  33. package/src/contributor-tools.test.ts +133 -0
  34. package/src/contributor-tools.ts +201 -0
  35. package/src/dashboard.test.ts +611 -0
  36. package/src/dashboard.ts +462 -0
  37. package/src/error-enrichment.test.ts +403 -0
  38. package/src/error-enrichment.ts +219 -0
  39. package/src/export-tools.test.ts +476 -0
  40. package/src/export-tools.ts +257 -0
  41. package/src/index.ts +8 -3
  42. package/src/query-tools.test.ts +636 -0
  43. package/src/query-tools.ts +324 -0
  44. package/src/replay-tools.test.ts +496 -0
  45. package/src/replay-tools.ts +240 -0
  46. package/src/sessions/agent-discovery.test.ts +137 -0
  47. package/src/sessions/agent-discovery.ts +112 -0
  48. package/src/sessions/index.ts +15 -0
@@ -0,0 +1,1215 @@
1
+ # ADR-010: CASS Inhousing Feasibility Study
2
+
3
+ > *"Refactoring is a controlled technique for improving the design of an existing code base. Its essence is applying a series of small behavior-preserving transformations, each of which 'too small to be worth doing'."*
4
+ > — Martin Fowler, Refactoring: Improving the Design of Existing Code
5
+
6
+ ## Status
7
+
8
+ **Proposed** (2025-12-26)
9
+
10
+ ## Context
11
+
12
+ ### Original Premise (INVALIDATED)
13
+ The original motivation was to "eliminate Python dependency" from CASS. **This premise was incorrect.** Research revealed that CASS is a **Rust application** (20K+ LOC), not Python.
14
+
15
+ ### Actual Opportunity
16
+ After deep architectural analysis, a different—and more compelling—opportunity emerged:
17
+
18
+ **We already have 90% of CASS's infrastructure in `semantic-memory`.**
19
+
20
+ Our existing semantic memory system (swarm-mail package) has:
21
+ - ✅ libSQL with F32_BLOB(1024) vectors + vector_top_k() ANN search
22
+ - ✅ Ollama embeddings (mxbai-embed-large, 1024 dims)
23
+ - ✅ FTS5 full-text search with auto-sync triggers
24
+ - ✅ Confidence decay (90-day half-life)
25
+ - ✅ Entity extraction + knowledge graph
26
+ - ✅ Collection filtering (namespace support)
27
+ - ✅ Batch embedding with controlled concurrency
28
+ - ✅ Graceful degradation (FTS5 fallback when Ollama down)
29
+
30
+ CASS provides:
31
+ - Session file parsing for 10+ agent types (Claude, Cursor, Codex, etc.)
32
+ - Message-level chunking
33
+ - File watching + auto-indexing
34
+ - Agent type discovery
35
+ - Session metadata schema
36
+ - Staleness detection
37
+ - Robot-mode API (token budgets, pagination, forgiving syntax)
38
+
39
+ **The gap is 8 thin adapters, not core infrastructure.**
40
+
41
+ ### Why Inhouse?
42
+
43
+ 1. **Eliminate External Binary Dependency** - One less install, one less config file, one less version to manage
44
+ 2. **Tighter Integration** - Swarm sessions auto-indexed, no export step
45
+ 3. **Unified Query API** - semantic-memory + session search in one tool
46
+ 4. **TDD-Friendly Architecture** - We control the test surface, can characterize behavior before refactoring
47
+ 5. **Incremental Migration** - Can build session indexing alongside existing CASS usage
48
+
49
+ ### Why NOT Full Rewrite?
50
+
51
+ CASS is production-quality software:
52
+ - 20K+ LOC Rust with Tantivy FTS engine
53
+ - 10 agent connectors with extensive test fixtures
54
+ - Robot-mode API with self-documenting commands
55
+ - Multi-machine sync (SSH, rsync, path mappings)
56
+ - TUI with syntax highlighting, fuzzy matching, sparklines
57
+
58
+ **Full inhousing would be a 4-week+ project.** That's not feasible.
59
+
60
+ ## Decision
61
+
62
+ **RECOMMEND: Partial Inhousing (Session Indexing Layer)**
63
+
64
+ Build a **session indexing layer** on top of our existing semantic-memory infrastructure. This gives us 80% of CASS's value with 20% of the implementation effort.
65
+
66
+ ### Scope
67
+
68
+ **IN SCOPE (Phase 1 - 2-3 days)**
69
+ 1. Session file parsing (JSONL-based agents: OpenCode Swarm, Cursor)
70
+ 2. Message-level chunking
71
+ 3. Metadata schema extension (agent_type, session_id, message_role, timestamp)
72
+ 4. File watching + auto-indexing (debounced, queued)
73
+ 5. Agent type discovery (path → agent mapping)
74
+ 6. Staleness detection (last_indexed vs file mtimes)
75
+ 7. Pagination API (fields="minimal")
76
+ 8. Session viewer (JSONL line reader)
77
+
78
+ **OUT OF SCOPE (Future)**
79
+ - Cloud-only agents (Claude Code, Gemini, Copilot - require API integration)
80
+ - Encrypted session formats (ChatGPT v2/v3 with macOS keychain)
81
+ - Multi-machine sync (SSH, rsync)
82
+ - TUI (robot-mode CLI only)
83
+ - Tantivy migration (FTS5 sufficient for now)
84
+
85
+ **DEPENDENCY: Existing CASS as Binary (Phase 0)**
86
+ Until session indexing is production-ready, keep current CASS usage via binary dependency. This allows incremental migration.
87
+
88
+ ## Architecture
89
+
90
+ ### System Diagram
91
+
92
+ ```
93
+ ┌─────────────────────────────────────────────────────────────────┐
94
+ │ SESSION INDEXING LAYER │
95
+ ├─────────────────────────────────────────────────────────────────┤
96
+ │ │
97
+ │ ┌─────────────┐ ┌──────────────┐ ┌─────────────┐ │
98
+ │ │ File │────▶│ Session │────▶│ Chunk │ │
99
+ │ │ Watcher │ │ Parser │ │ Processor │ │
100
+ │ │ │ │ (JSONL) │ │ (Messages) │ │
101
+ │ └─────────────┘ └──────────────┘ └─────────────┘ │
102
+ │ │ │ │ │
103
+ │ │ │ ▼ │
104
+ │ │ │ ┌─────────────┐ │
105
+ │ │ │ │ Embedding │ │
106
+ │ │ │ │ Pipeline │ │
107
+ │ │ │ │ (Ollama) │ │
108
+ │ │ │ └─────────────┘ │
109
+ │ │ │ │ │
110
+ │ ▼ ▼ ▼ │
111
+ │ ┌──────────────────────────────────────────────────────┐ │
112
+ │ │ SEMANTIC MEMORY (libSQL + FTS5) │ │
113
+ │ │ │ │
114
+ │ │ - memories table (extended with session metadata) │ │
115
+ │ │ - memories_fts (full-text search) │ │
116
+ │ │ - vector_top_k() (ANN search) │ │
117
+ │ │ - confidence_decay() (recency scoring) │ │
118
+ │ └──────────────────────────────────────────────────────┘ │
119
+ │ │ │
120
+ │ ▼ │
121
+ │ ┌─────────────────┐ │
122
+ │ │ Query Interface │ │
123
+ │ │ (MCP Tools) │ │
124
+ │ │ │ │
125
+ │ │ - cass_search │ │
126
+ │ │ - cass_view │ │
127
+ │ │ - cass_expand │ │
128
+ │ │ - cass_index │ │
129
+ │ │ - cass_health │ │
130
+ │ └─────────────────┘ │
131
+ │ │
132
+ └─────────────────────────────────────────────────────────────────┘
133
+ ```
134
+
135
+ ### Component Responsibilities
136
+
137
+ #### 1. File Watcher
138
+ **Purpose:** Monitor session directories for new/modified JSONL files
139
+
140
+ **Directories to watch:**
141
+ - `~/.config/swarm-tools/sessions/` (OpenCode Swarm)
142
+ - `~/Library/Application Support/Cursor/User/History/` (Cursor)
143
+ - `~/.opencode/` (OpenCode - recursive scan)
144
+
145
+ **Implementation:**
146
+ - Use Node.js `fs.watch()` or `chokidar` for cross-platform watching
147
+ - Debounce: 500ms (batch rapid file changes)
148
+ - Queue: concurrent indexing with limit=5 (prevent Ollama overload)
149
+
150
+ **TDD Approach:**
151
+ ```typescript
152
+ // RED: Write failing test
153
+ describe('FileWatcher', () => {
154
+ test('detects new JSONL file in watched directory', async () => {
155
+ const watcher = new FileWatcher(['/tmp/sessions']);
156
+ const detected = vi.fn();
157
+ watcher.on('file-added', detected);
158
+
159
+ await watcher.start();
160
+ await fs.writeFile('/tmp/sessions/new.jsonl', '{}');
161
+
162
+ await vi.waitFor(() => expect(detected).toHaveBeenCalledWith({
163
+ path: '/tmp/sessions/new.jsonl',
164
+ event: 'added'
165
+ }));
166
+ });
167
+
168
+ test('debounces rapid file changes', async () => {
169
+ const watcher = new FileWatcher(['/tmp/sessions'], { debounce: 100 });
170
+ const detected = vi.fn();
171
+ watcher.on('file-changed', detected);
172
+
173
+ await watcher.start();
174
+
175
+ // Rapid writes (should batch)
176
+ await fs.writeFile('/tmp/sessions/ses_1.jsonl', '{"id":1}');
177
+ await fs.writeFile('/tmp/sessions/ses_1.jsonl', '{"id":2}');
178
+ await fs.writeFile('/tmp/sessions/ses_1.jsonl', '{"id":3}');
179
+
180
+ await vi.waitFor(() => expect(detected).toHaveBeenCalledTimes(1), { timeout: 200 });
181
+ });
182
+ });
183
+
184
+ // GREEN: Implement minimal watcher
185
+ class FileWatcher extends EventEmitter {
186
+ constructor(paths: string[], opts = { debounce: 500 }) { /* ... */ }
187
+ start() { /* chokidar.watch() */ }
188
+ stop() { /* watcher.close() */ }
189
+ }
190
+
191
+ // REFACTOR: Extract debouncing, add error handling
192
+ ```
193
+
194
+ #### 2. Session Parser
195
+ **Purpose:** Parse JSONL session files into normalized messages
196
+
197
+ **Supported formats (Phase 1):**
198
+ - **OpenCode Swarm**: `{session_id, event_type, timestamp, payload}`
199
+ - **Cursor**: `{type, timestamp, content}` (needs investigation)
200
+
201
+ **Normalization schema:**
202
+ ```typescript
203
+ interface NormalizedMessage {
204
+ session_id: string; // File-derived or parsed
205
+ agent_type: string; // 'opencode-swarm' | 'cursor' | ...
206
+ message_idx: number; // Line number in JSONL
207
+ timestamp: string; // ISO 8601
208
+ role: 'user' | 'assistant' | 'system';
209
+ content: string; // Extracted text
210
+ metadata: Record<string, unknown>; // Agent-specific fields
211
+ }
212
+ ```
213
+
214
+ **TDD Approach:**
215
+ ```typescript
216
+ // RED: Test OpenCode Swarm parser
217
+ describe('SessionParser', () => {
218
+ test('parses OpenCode Swarm JSONL format', async () => {
219
+ const jsonl = [
220
+ '{"session_id":"ses_123","event_type":"DECISION","timestamp":"2025-12-26T10:00:00Z","payload":{"action":"spawn"}}',
221
+ '{"session_id":"ses_123","event_type":"OUTCOME","timestamp":"2025-12-26T10:01:00Z","payload":{"status":"success"}}'
222
+ ].join('\n');
223
+
224
+ const parser = new SessionParser('opencode-swarm');
225
+ const messages = await parser.parse(jsonl, { filePath: 'ses_123.jsonl' });
226
+
227
+ expect(messages).toHaveLength(2);
228
+ expect(messages[0]).toMatchObject({
229
+ session_id: 'ses_123',
230
+ agent_type: 'opencode-swarm',
231
+ message_idx: 0,
232
+ timestamp: '2025-12-26T10:00:00Z',
233
+ role: 'system',
234
+ content: 'DECISION: spawn'
235
+ });
236
+ });
237
+
238
+ test('handles malformed JSONL gracefully', async () => {
239
+ const jsonl = [
240
+ '{"valid": "json"}',
241
+ 'INVALID JSON',
242
+ '{"valid": "json2"}'
243
+ ].join('\n');
244
+
245
+ const parser = new SessionParser('opencode-swarm');
246
+ const messages = await parser.parse(jsonl);
247
+
248
+ expect(messages).toHaveLength(2); // Skips malformed line
249
+ });
250
+ });
251
+
252
+ // GREEN: Implement basic parser
253
+ class SessionParser {
254
+ constructor(private agentType: string) {}
255
+
256
+ async parse(jsonl: string, opts?: { filePath?: string }): Promise<NormalizedMessage[]> {
257
+ return jsonl.split('\n')
258
+ .map((line, idx) => {
259
+ try {
260
+ const obj = JSON.parse(line);
261
+ return this.normalize(obj, idx);
262
+ } catch {
263
+ return null; // Skip malformed
264
+ }
265
+ })
266
+ .filter(Boolean);
267
+ }
268
+
269
+ private normalize(obj: any, idx: number): NormalizedMessage { /* ... */ }
270
+ }
271
+
272
+ // REFACTOR: Add agent-specific parsers, extract normalize()
273
+ ```
274
+
275
+ #### 3. Chunk Processor
276
+ **Purpose:** Split sessions into searchable message-level chunks, embed with Ollama
277
+
278
+ **Chunking strategy:**
279
+ - 1 chunk = 1 message (no further splitting for now)
280
+ - Future: Split long messages at sentence boundaries if >2000 tokens
281
+
282
+ **Embedding:**
283
+ - Reuse existing `BatchEmbedder` from semantic-memory
284
+ - Controlled concurrency (5 concurrent requests to Ollama)
285
+ - Graceful degradation (store without embeddings if Ollama down)
286
+
287
+ **TDD Approach:**
288
+ ```typescript
289
+ // RED: Test chunking
290
+ describe('ChunkProcessor', () => {
291
+ test('creates one chunk per message', async () => {
292
+ const messages = [
293
+ { session_id: 's1', content: 'Hello', timestamp: '2025-12-26T10:00:00Z', role: 'user' },
294
+ { session_id: 's1', content: 'Hi there', timestamp: '2025-12-26T10:01:00Z', role: 'assistant' }
295
+ ];
296
+
297
+ const processor = new ChunkProcessor();
298
+ const chunks = await processor.chunk(messages);
299
+
300
+ expect(chunks).toHaveLength(2);
301
+ expect(chunks[0].content).toBe('Hello');
302
+ expect(chunks[1].content).toBe('Hi there');
303
+ });
304
+
305
+ test('embeds chunks with Ollama', async () => {
306
+ const chunks = [{ content: 'Test message' }];
307
+ const processor = new ChunkProcessor({ embedder: mockOllamaClient });
308
+
309
+ const embedded = await processor.embed(chunks);
310
+
311
+ expect(embedded[0].vector).toHaveLength(1024);
312
+ expect(mockOllamaClient.embed).toHaveBeenCalledWith('Test message');
313
+ });
314
+
315
+ test('gracefully handles Ollama failure', async () => {
316
+ const chunks = [{ content: 'Test' }];
317
+ const processor = new ChunkProcessor({ embedder: failingOllamaClient });
318
+
319
+ const embedded = await processor.embed(chunks);
320
+
321
+ expect(embedded[0].vector).toBeNull(); // Store without embedding
322
+ });
323
+ });
324
+
325
+ // GREEN: Implement processor
326
+ class ChunkProcessor {
327
+ constructor(private opts = { embedder: getOllamaClient() }) {}
328
+
329
+ async chunk(messages: NormalizedMessage[]): Promise<Chunk[]> {
330
+ return messages.map(msg => ({ ...msg })); // 1:1 for now
331
+ }
332
+
333
+ async embed(chunks: Chunk[]): Promise<EmbeddedChunk[]> {
334
+ try {
335
+ const vectors = await this.opts.embedder.embedBatch(chunks.map(c => c.content));
336
+ return chunks.map((chunk, i) => ({ ...chunk, vector: vectors[i] }));
337
+ } catch (err) {
338
+ return chunks.map(chunk => ({ ...chunk, vector: null }));
339
+ }
340
+ }
341
+ }
342
+
343
+ // REFACTOR: Add batch size limits, retry logic
344
+ ```
345
+
346
+ #### 4. Metadata Schema Extension
347
+ **Purpose:** Extend `memories` table to support session-specific fields
348
+
349
+ **SQL Migration:**
350
+ ```sql
351
+ -- Migration: Add session metadata columns
352
+ ALTER TABLE memories ADD COLUMN agent_type TEXT;
353
+ ALTER TABLE memories ADD COLUMN session_id TEXT;
354
+ ALTER TABLE memories ADD COLUMN message_role TEXT CHECK (message_role IN ('user', 'assistant', 'system'));
355
+ ALTER TABLE memories ADD COLUMN message_idx INTEGER;
356
+ ALTER TABLE memories ADD COLUMN source_path TEXT;
357
+
358
+ -- Index for fast agent filtering
359
+ CREATE INDEX idx_memories_agent_type ON memories(agent_type) WHERE agent_type IS NOT NULL;
360
+
361
+ -- Index for session lookup
362
+ CREATE INDEX idx_memories_session_id ON memories(session_id) WHERE session_id IS NOT NULL;
363
+
364
+ -- Update FTS5 to index agent_type
365
+ INSERT INTO memories_fts(memories_fts) VALUES('rebuild');
366
+ ```
367
+
368
+ **TDD Approach:**
369
+ ```typescript
370
+ // RED: Test schema migration
371
+ describe('Session Metadata Schema', () => {
372
+ test('stores session-specific metadata', async () => {
373
+ const db = await createInMemorySwarmMail();
374
+
375
+ await db.storeMemory({
376
+ information: 'Test message',
377
+ metadata: {
378
+ agent_type: 'opencode-swarm',
379
+ session_id: 'ses_123',
380
+ message_role: 'assistant',
381
+ message_idx: 5,
382
+ source_path: '/path/to/ses_123.jsonl'
383
+ }
384
+ });
385
+
386
+ const results = await db.findMemories({ query: 'test', agent_type: 'opencode-swarm' });
387
+ expect(results[0].metadata.agent_type).toBe('opencode-swarm');
388
+ expect(results[0].metadata.session_id).toBe('ses_123');
389
+ });
390
+
391
+ test('filters by agent type', async () => {
392
+ const db = await createInMemorySwarmMail();
393
+
394
+ await db.storeMemory({ information: 'Swarm msg', metadata: { agent_type: 'opencode-swarm' } });
395
+ await db.storeMemory({ information: 'Cursor msg', metadata: { agent_type: 'cursor' } });
396
+
397
+ const results = await db.findMemories({ query: 'msg', agent_type: 'cursor' });
398
+
399
+ expect(results).toHaveLength(1);
400
+ expect(results[0].metadata.agent_type).toBe('cursor');
401
+ });
402
+ });
403
+
404
+ // GREEN: Add migration + query support
405
+ // REFACTOR: Extract agent_type enum, add validation
406
+ ```
407
+
408
+ #### 5. Agent Type Discovery
409
+ **Purpose:** Map file paths to agent types
410
+
411
+ **Mapping rules:**
412
+ ```typescript
413
+ const AGENT_PATH_PATTERNS = [
414
+ { pattern: /\.config\/swarm-tools\/sessions\//, agentType: 'opencode-swarm' },
415
+ { pattern: /Cursor\/User\/History\//, agentType: 'cursor' },
416
+ { pattern: /\.opencode\//, agentType: 'opencode' },
417
+ { pattern: /\.local\/share\/Claude\//, agentType: 'claude' },
418
+ { pattern: /\.aider/, agentType: 'aider' },
419
+ ];
420
+
421
+ function detectAgentType(filePath: string): string | null {
422
+ for (const { pattern, agentType } of AGENT_PATH_PATTERNS) {
423
+ if (pattern.test(filePath)) return agentType;
424
+ }
425
+ return null;
426
+ }
427
+ ```
428
+
429
+ **TDD Approach:**
430
+ ```typescript
431
+ // RED: Test agent detection
432
+ describe('detectAgentType', () => {
433
+ test('detects OpenCode Swarm sessions', () => {
434
+ expect(detectAgentType('/home/user/.config/swarm-tools/sessions/ses_123.jsonl'))
435
+ .toBe('opencode-swarm');
436
+ });
437
+
438
+ test('detects Cursor sessions', () => {
439
+ expect(detectAgentType('/Users/joel/Library/Application Support/Cursor/User/History/abc/9ScS.jsonl'))
440
+ .toBe('cursor');
441
+ });
442
+
443
+ test('returns null for unknown paths', () => {
444
+ expect(detectAgentType('/tmp/random.jsonl')).toBeNull();
445
+ });
446
+ });
447
+
448
+ // GREEN: Implement pattern matching
449
+ // REFACTOR: Load patterns from config file
450
+ ```
451
+
452
+ #### 6. Staleness Detection
453
+ **Purpose:** Track last index time vs file mtimes, report when stale
454
+
455
+ **Schema:**
456
+ ```sql
457
+ CREATE TABLE IF NOT EXISTS session_index_state (
458
+ source_path TEXT PRIMARY KEY,
459
+ last_indexed_at INTEGER NOT NULL, -- Unix timestamp
460
+ file_mtime INTEGER NOT NULL,
461
+ message_count INTEGER NOT NULL
462
+ );
463
+ ```
464
+
465
+ **Staleness definition:** `file_mtime > last_indexed_at + 300` (5 min grace period)
466
+
467
+ **TDD Approach:**
468
+ ```typescript
469
+ // RED: Test staleness detection
470
+ describe('StalenessDetector', () => {
471
+ test('reports file as stale when mtime > last_indexed + 300s', async () => {
472
+ const db = await createInMemorySwarmMail();
473
+ const detector = new StalenessDetector(db);
474
+
475
+ // Index file at T=0
476
+ await detector.recordIndexed('/tmp/ses_1.jsonl', { mtime: 1000, messageCount: 10 });
477
+
478
+ // File modified at T=400
479
+ vi.setSystemTime(new Date(1400 * 1000));
480
+ const stale = await detector.checkStaleness('/tmp/ses_1.jsonl', { currentMtime: 1400 });
481
+
482
+ expect(stale).toBe(true);
483
+ });
484
+
485
+ test('reports file as fresh when mtime within grace period', async () => {
486
+ const db = await createInMemorySwarmMail();
487
+ const detector = new StalenessDetector(db);
488
+
489
+ await detector.recordIndexed('/tmp/ses_1.jsonl', { mtime: 1000, messageCount: 10 });
490
+
491
+ vi.setSystemTime(new Date(1200 * 1000));
492
+ const stale = await detector.checkStaleness('/tmp/ses_1.jsonl', { currentMtime: 1000 });
493
+
494
+ expect(stale).toBe(false);
495
+ });
496
+ });
497
+
498
+ // GREEN: Implement detector
499
+ class StalenessDetector {
500
+ async recordIndexed(path: string, opts: { mtime: number, messageCount: number }) { /* ... */ }
501
+ async checkStaleness(path: string, opts: { currentMtime: number }): Promise<boolean> { /* ... */ }
502
+ }
503
+
504
+ // REFACTOR: Add bulk staleness check, configurable grace period
505
+ ```
506
+
507
+ #### 7. Pagination API
508
+ **Purpose:** Support `fields="minimal"` for compact output
509
+
510
+ **Field sets:**
511
+ ```typescript
512
+ const FIELD_SETS = {
513
+ minimal: ['source_path', 'message_idx', 'agent_type'],
514
+ summary: ['source_path', 'message_idx', 'agent_type', 'timestamp', 'role', 'preview'],
515
+ full: '*', // All columns
516
+ };
517
+ ```
518
+
519
+ **TDD Approach:**
520
+ ```typescript
521
+ // RED: Test field filtering
522
+ describe('Session Query API', () => {
523
+ test('returns minimal fields when fields="minimal"', async () => {
524
+ const db = await createInMemorySwarmMail();
525
+ await db.indexSession('/path/to/ses_1.jsonl');
526
+
527
+ const results = await db.searchSessions({ query: 'test', fields: 'minimal' });
528
+
529
+ expect(results[0]).toEqual({
530
+ source_path: '/path/to/ses_1.jsonl',
531
+ message_idx: 0,
532
+ agent_type: 'opencode-swarm'
533
+ });
534
+ expect(results[0].content).toBeUndefined();
535
+ });
536
+
537
+ test('supports custom field list', async () => {
538
+ const db = await createInMemorySwarmMail();
539
+ await db.indexSession('/path/to/ses_1.jsonl');
540
+
541
+ const results = await db.searchSessions({
542
+ query: 'test',
543
+ fields: ['source_path', 'timestamp', 'content']
544
+ });
545
+
546
+ expect(Object.keys(results[0])).toEqual(['source_path', 'timestamp', 'content']);
547
+ });
548
+ });
549
+
550
+ // GREEN: Implement field projection
551
+ // REFACTOR: Add TypeScript type narrowing for field sets
552
+ ```
553
+
554
+ #### 8. Session Viewer
555
+ **Purpose:** Read JSONL file, extract specific line range, format for display
556
+
557
+ **API:**
558
+ ```typescript
559
+ interface SessionViewerOpts {
560
+ path: string; // Absolute path to JSONL file
561
+ line?: number; // Target line (1-indexed)
562
+ context?: number; // Lines before/after (default: 3)
563
+ }
564
+
565
+ async function viewSession(opts: SessionViewerOpts): Promise<string>
566
+ ```
567
+
568
+ **TDD Approach:**
569
+ ```typescript
570
+ // RED: Test line extraction
571
+ describe('SessionViewer', () => {
572
+ test('extracts single line from JSONL', async () => {
573
+ const jsonl = [
574
+ '{"id":1,"msg":"First"}',
575
+ '{"id":2,"msg":"Second"}',
576
+ '{"id":3,"msg":"Third"}'
577
+ ].join('\n');
578
+ await fs.writeFile('/tmp/ses.jsonl', jsonl);
579
+
580
+ const viewer = new SessionViewer();
581
+ const result = await viewer.view({ path: '/tmp/ses.jsonl', line: 2 });
582
+
583
+ expect(result).toContain('{"id":2,"msg":"Second"}');
584
+ });
585
+
586
+ test('includes context lines', async () => {
587
+ const jsonl = Array.from({ length: 10 }, (_, i) => `{"id":${i}}`).join('\n');
588
+ await fs.writeFile('/tmp/ses.jsonl', jsonl);
589
+
590
+ const viewer = new SessionViewer();
591
+ const result = await viewer.view({ path: '/tmp/ses.jsonl', line: 5, context: 2 });
592
+
593
+ expect(result).toContain('{"id":3}'); // line - 2
594
+ expect(result).toContain('{"id":4}'); // line - 1
595
+ expect(result).toContain('{"id":5}'); // target line
596
+ expect(result).toContain('{"id":6}'); // line + 1
597
+ expect(result).toContain('{"id":7}'); // line + 2
598
+ });
599
+
600
+ test('handles line number out of range', async () => {
601
+ await fs.writeFile('/tmp/ses.jsonl', '{"id":1}\n{"id":2}');
602
+
603
+ const viewer = new SessionViewer();
604
+ await expect(viewer.view({ path: '/tmp/ses.jsonl', line: 100 }))
605
+ .rejects.toThrow('Line 100 not found');
606
+ });
607
+ });
608
+
609
+ // GREEN: Implement line reader
610
+ class SessionViewer {
611
+ async view(opts: SessionViewerOpts): Promise<string> {
612
+ const lines = (await fs.readFile(opts.path, 'utf-8')).split('\n');
613
+ const lineIdx = (opts.line ?? 1) - 1;
614
+ const context = opts.context ?? 3;
615
+
616
+ if (lineIdx < 0 || lineIdx >= lines.length) {
617
+ throw new Error(`Line ${opts.line} not found in ${opts.path}`);
618
+ }
619
+
620
+ const start = Math.max(0, lineIdx - context);
621
+ const end = Math.min(lines.length, lineIdx + context + 1);
622
+
623
+ return lines.slice(start, end)
624
+ .map((line, i) => `${start + i + 1}: ${line}`)
625
+ .join('\n');
626
+ }
627
+ }
628
+
629
+ // REFACTOR: Add syntax highlighting, JSON pretty-printing
630
+ ```
631
+
632
+ ### Integration with Existing semantic-memory
633
+
634
+ **Reuse 100%:**
635
+ - `SwarmDb` client (libSQL connection pooling)
636
+ - `BatchEmbedder` (Ollama client with retry + concurrency control)
637
+ - `memories` table schema (extend with new columns)
638
+ - `findMemories()` API (add agent_type, session_id filters)
639
+ - `storeMemory()` API (validate session metadata)
640
+ - Confidence decay mechanism (repurpose for message recency)
641
+
642
+ **New Modules:**
643
+ ```
644
+ swarm-mail/src/
645
+ sessions/
646
+ file-watcher.ts # Watch session directories
647
+ session-parser.ts # Parse JSONL formats
648
+ chunk-processor.ts # Chunk + embed messages
649
+ agent-discovery.ts # Path → agent type mapping
650
+ staleness-detector.ts # Track index freshness
651
+ session-viewer.ts # JSONL line reader
652
+ session-indexer.ts # Orchestrates above components
653
+ index.ts # Public API exports
654
+ ```
655
+
656
+ **MCP Tool Wrappers:**
657
+ ```
658
+ opencode-swarm-plugin/src/
659
+ sessions.ts # MCP tool implementations
660
+ - cass_search() # Wraps SessionIndexer.search()
661
+ - cass_view() # Wraps SessionViewer.view()
662
+ - cass_expand() # Wraps SessionViewer.view(context=N)
663
+ - cass_index() # Triggers SessionIndexer.indexAll()
664
+ - cass_health() # Checks staleness, index stats
665
+ - cass_stats() # Session counts by agent type
666
+ ```
667
+
668
+ ## TDD Implementation Plan
669
+
670
+ ### Phase 0: Characterization Tests (BEFORE touching code)
671
+
672
+ **Goal:** Document existing CASS behavior to prevent regression during migration.
673
+
674
+ **Tests to write:**
675
+ 1. **Search behavior:**
676
+ ```bash
677
+ # Capture baseline search results
678
+ cass search "authentication error" --agent opencode --days 7 --robot > baseline_search.json
679
+ ```
680
+
681
+ 2. **View behavior:**
682
+ ```bash
683
+ cass view ~/.config/swarm-tools/sessions/ses_123.jsonl -n 5 -C 3 > baseline_view.txt
684
+ ```
685
+
686
+ 3. **Health check:**
687
+ ```bash
688
+ cass health > baseline_health.txt
689
+ ```
690
+
691
+ **Characterization test suite:**
692
+ ```typescript
693
+ describe('CASS Baseline Behavior (Characterization)', () => {
694
+ test('search returns expected structure', async () => {
695
+ const result = await exec('cass search "test" --robot');
696
+ const json = JSON.parse(result.stdout);
697
+
698
+ expect(json).toMatchObject({
699
+ results: expect.arrayContaining([
700
+ expect.objectContaining({
701
+ path: expect.any(String),
702
+ line: expect.any(Number),
703
+ agent: expect.any(String),
704
+ score: expect.any(Number),
705
+ })
706
+ ]),
707
+ total: expect.any(Number)
708
+ });
709
+ });
710
+
711
+ test('health check reports index status', async () => {
712
+ const result = await exec('cass health');
713
+ expect(result.stdout).toMatch(/Index: (ready|needs indexing)/);
714
+ });
715
+ });
716
+ ```
717
+
718
+ ### Phase 1: Foundation (Day 1)
719
+
720
+ **Goal:** Build core infrastructure without file watching.
721
+
722
+ #### 1.1 Session Parser (TDD)
723
+ - ✅ RED: Write test for OpenCode Swarm JSONL parsing
724
+ - ✅ GREEN: Implement minimal parser
725
+ - ✅ REFACTOR: Extract normalization logic, add error handling
726
+ - ✅ RED: Test malformed JSONL handling
727
+ - ✅ GREEN: Skip invalid lines gracefully
728
+ - ✅ REFACTOR: Add logging for skipped lines
729
+
730
+ #### 1.2 Metadata Schema (TDD)
731
+ - ✅ RED: Write test for storing session metadata
732
+ - ✅ GREEN: Add SQL migration, extend storeMemory()
733
+ - ✅ REFACTOR: Add Zod validation for session metadata
734
+ - ✅ RED: Test filtering by agent_type
735
+ - ✅ GREEN: Add WHERE clause to findMemories()
736
+ - ✅ REFACTOR: Index optimization for agent_type queries
737
+
738
+ #### 1.3 Chunk Processor (TDD)
739
+ - ✅ RED: Test message chunking (1:1 for now)
740
+ - ✅ GREEN: Implement basic chunker
741
+ - ✅ REFACTOR: Extract chunking strategy interface
742
+ - ✅ RED: Test Ollama embedding integration
743
+ - ✅ GREEN: Reuse BatchEmbedder from semantic-memory
744
+ - ✅ REFACTOR: Add graceful degradation (FTS5 fallback)
745
+
746
+ **Validation:** Manual indexing of a single JSONL file
747
+ ```typescript
748
+ import { SessionIndexer } from 'swarm-mail/sessions';
749
+
750
+ const indexer = new SessionIndexer();
751
+ await indexer.indexFile('/path/to/ses_123.jsonl');
752
+
753
+ const results = await indexer.search({ query: 'test', agent_type: 'opencode-swarm' });
754
+ console.log(results); // Should return messages from ses_123.jsonl
755
+ ```
756
+
757
+ ### Phase 2: Automation (Day 2)
758
+
759
+ **Goal:** Add file watching and auto-indexing.
760
+
761
+ #### 2.1 File Watcher (TDD)
762
+ - ✅ RED: Test detection of new JSONL file
763
+ - ✅ GREEN: Implement chokidar-based watcher
764
+ - ✅ REFACTOR: Add debouncing (500ms)
765
+ - ✅ RED: Test debouncing of rapid changes
766
+ - ✅ GREEN: Batch file events
767
+ - ✅ REFACTOR: Add error recovery, restart logic
768
+
769
+ #### 2.2 Agent Discovery (TDD)
770
+ - ✅ RED: Test path → agent type mapping
771
+ - ✅ GREEN: Implement pattern matching
772
+ - ✅ REFACTOR: Load patterns from config file
773
+ - ✅ RED: Test unknown path handling
774
+ - ✅ GREEN: Return null for unknown agents
775
+ - ✅ REFACTOR: Add user-defined patterns
776
+
777
+ #### 2.3 Staleness Detection (TDD)
778
+ - ✅ RED: Test staleness detection logic
779
+ - ✅ GREEN: Implement mtime comparison
780
+ - ✅ REFACTOR: Add grace period (300s)
781
+ - ✅ RED: Test bulk staleness check
782
+ - ✅ GREEN: Optimize with batch queries
783
+ - ✅ REFACTOR: Add staleness metrics
784
+
785
+ **Validation:** Start watcher, modify JSONL file, verify auto-reindex
786
+ ```typescript
787
+ const watcher = new FileWatcher(['/path/to/sessions']);
788
+ watcher.start();
789
+
790
+ // Modify file
791
+ await fs.appendFile('/path/to/ses_123.jsonl', '\n{"new":"message"}');
792
+
793
+ // Wait for auto-index
794
+ await vi.waitFor(() => {
795
+ const results = indexer.search({ query: 'new message' });
796
+ expect(results.length).toBeGreaterThan(0);
797
+ });
798
+ ```
799
+
800
+ ### Phase 3: API + Tools (Day 3)
801
+
802
+ **Goal:** Build MCP tools and validate against characterization tests.
803
+
804
+ #### 3.1 Session Viewer (TDD)
805
+ - ✅ RED: Test JSONL line extraction
806
+ - ✅ GREEN: Implement line reader
807
+ - ✅ REFACTOR: Add context parameter
808
+ - ✅ RED: Test syntax highlighting
809
+ - ✅ GREEN: Add JSON pretty-printing
810
+ - ✅ REFACTOR: Support multiple output formats
811
+
812
+ #### 3.2 Pagination API (TDD)
813
+ - ✅ RED: Test fields="minimal" output
814
+ - ✅ GREEN: Implement field projection
815
+ - ✅ REFACTOR: Add TypeScript type narrowing
816
+ - ✅ RED: Test custom field lists
817
+ - ✅ GREEN: Support array of field names
818
+ - ✅ REFACTOR: Add field validation
819
+
820
+ #### 3.3 MCP Tools (TDD)
821
+ - ✅ RED: Test cass_search tool
822
+ - ✅ GREEN: Implement search wrapper
823
+ - ✅ REFACTOR: Add token budget limits
824
+ - ✅ RED: Test cass_view tool
825
+ - ✅ GREEN: Implement viewer wrapper
826
+ - ✅ REFACTOR: Add error formatting
827
+ - ✅ RED: Test cass_health tool
828
+ - ✅ GREEN: Implement health checker
829
+ - ✅ REFACTOR: Add detailed diagnostics
830
+
831
+ **Validation:** Run characterization tests against new implementation
832
+ ```typescript
833
+ describe('Session Indexing vs CASS Baseline', () => {
834
+ test('search results match CASS structure', async () => {
835
+ const newResults = await cass_search({ query: 'test', agent: 'opencode' });
836
+ const baseline = JSON.parse(fs.readFileSync('baseline_search.json'));
837
+
838
+ expect(newResults).toMatchObject({
839
+ results: expect.arrayContaining([
840
+ expect.objectContaining({
841
+ source_path: expect.any(String),
842
+ message_idx: expect.any(Number),
843
+ agent_type: expect.any(String),
844
+ })
845
+ ])
846
+ });
847
+ });
848
+ });
849
+ ```
850
+
851
+ ### Phase 4: Migration & Observability (Day 3-4)
852
+
853
+ **Goal:** Migrate from binary CASS to inhouse, add observability.
854
+
855
+ #### 4.1 Dual-Mode Support
856
+ **Strategy:** Support both binary CASS and inhouse session indexing during transition.
857
+
858
+ ```typescript
859
+ // config.ts
860
+ interface SessionIndexConfig {
861
+ mode: 'binary' | 'inhouse' | 'hybrid';
862
+ binaryPath?: string; // Path to CASS binary (for binary/hybrid mode)
863
+ watchPaths: string[]; // Directories to watch (inhouse mode)
864
+ agentPatterns: AgentPattern[]; // Path → agent type mappings
865
+ }
866
+
867
+ // Hybrid mode: Try inhouse first, fallback to binary
868
+ async function cass_search(opts: SearchOpts) {
869
+ if (config.mode === 'inhouse' || config.mode === 'hybrid') {
870
+ try {
871
+ return await sessionIndexer.search(opts);
872
+ } catch (err) {
873
+ if (config.mode === 'inhouse') throw err;
874
+ // Fallback to binary in hybrid mode
875
+ }
876
+ }
877
+
878
+ // Binary mode or hybrid fallback
879
+ return execCassBinary(['search', opts.query, ...buildArgs(opts)]);
880
+ }
881
+ ```
882
+
883
+ #### 4.2 Observability (Align with ADR-005)
884
+
885
+ **Metrics to track:**
886
+ ```typescript
887
+ // Using Pino logger from ADR-005
888
+ logger.info({
889
+ component: 'session-indexer',
890
+ event: 'file-indexed',
891
+ source_path: filePath,
892
+ agent_type: agentType,
893
+ message_count: messages.length,
894
+ duration_ms: Date.now() - startTime,
895
+ embedding_enabled: hasEmbeddings
896
+ });
897
+
898
+ logger.warn({
899
+ component: 'session-indexer',
900
+ event: 'staleness-detected',
901
+ source_path: filePath,
902
+ last_indexed_at: lastIndexed,
903
+ file_mtime: currentMtime,
904
+ age_seconds: currentMtime - lastIndexed
905
+ });
906
+
907
+ logger.error({
908
+ component: 'session-parser',
909
+ event: 'parse-failure',
910
+ source_path: filePath,
911
+ line_number: lineNum,
912
+ error: err.message
913
+ });
914
+ ```
915
+
916
+ **Health metrics:**
917
+ ```typescript
918
+ interface SessionIndexHealth {
919
+ status: 'ready' | 'degraded' | 'error';
920
+ total_sessions: number;
921
+ indexed_sessions: number;
922
+ stale_sessions: number;
923
+ agent_breakdown: Record<string, number>;
924
+ last_index_duration_ms: number;
925
+ embedding_enabled: boolean;
926
+ }
927
+ ```
928
+
929
+ #### 4.3 Migration Checklist
930
+
931
+ **Pre-migration:**
932
+ - [ ] Run characterization tests against binary CASS
933
+ - [ ] Capture baseline performance metrics (index time, search latency)
934
+ - [ ] Document current CASS usage in codebase (grep for `cass_*`)
935
+
936
+ **Migration:**
937
+ - [ ] Set `mode: 'hybrid'` in config (inhouse with binary fallback)
938
+ - [ ] Index existing sessions with inhouse indexer
939
+ - [ ] Validate search results match binary CASS
940
+ - [ ] Monitor error logs for inhouse failures
941
+ - [ ] Run comparison tests (inhouse vs binary search results)
942
+
943
+ **Post-migration:**
944
+ - [ ] Set `mode: 'inhouse'` after 1 week of stable hybrid operation
945
+ - [ ] Remove binary CASS from dependencies
946
+ - [ ] Archive characterization tests (keep for regression)
947
+ - [ ] Update documentation (remove binary CASS instructions)
948
+
949
+ ## Migration Path
950
+
951
+ ### Timeline
952
+
953
+ | Phase | Duration | Deliverable |
954
+ |-------|----------|-------------|
955
+ | Phase 0: Characterization | 2 hours | Baseline test suite |
956
+ | Phase 1: Foundation | 1 day | Manual session indexing working |
957
+ | Phase 2: Automation | 1 day | File watching + auto-indexing |
958
+ | Phase 3: API + Tools | 1 day | MCP tools complete |
959
+ | Phase 4: Migration | 0.5 day | Hybrid mode validated |
960
+ | **Total** | **3.5 days** | Production-ready inhouse CASS |
961
+
962
+ ### Subtask Breakdown (for Epic Creation)
963
+
964
+ 1. **T1: Characterization Tests** (2h)
965
+ - Files: `evals/fixtures/cass-baseline.ts`, `bin/cass.characterization.test.ts`
966
+ - Tests: Search, view, health baselines
967
+
968
+ 2. **T2: Session Parser + Metadata Schema** (4h)
969
+ - Files: `swarm-mail/src/sessions/session-parser.ts`, `swarm-mail/src/sessions/session-parser.test.ts`
970
+ - SQL: Add agent_type, session_id, message_role columns
971
+
972
+ 3. **T3: Chunk Processor** (3h)
973
+ - Files: `swarm-mail/src/sessions/chunk-processor.ts`, `.test.ts`
974
+ - Integration: Reuse BatchEmbedder
975
+
976
+ 4. **T4: File Watcher + Agent Discovery** (4h)
977
+ - Files: `swarm-mail/src/sessions/file-watcher.ts`, `agent-discovery.ts`, `.test.ts`
978
+ - Config: Add watch paths, agent patterns
979
+
980
+ 5. **T5: Staleness Detector** (2h)
981
+ - Files: `swarm-mail/src/sessions/staleness-detector.ts`, `.test.ts`
982
+ - SQL: Add session_index_state table
983
+
984
+ 6. **T6: Session Viewer** (2h)
985
+ - Files: `swarm-mail/src/sessions/session-viewer.ts`, `.test.ts`
986
+ - Features: Line extraction, context, syntax highlighting
987
+
988
+ 7. **T7: Pagination API** (2h)
989
+ - Files: `swarm-mail/src/adapter.ts` (extend findMemories)
990
+ - API: Add fields parameter, field sets
991
+
992
+ 8. **T8: MCP Tools** (4h)
993
+ - Files: `src/sessions.ts` (cass_search, cass_view, cass_expand, cass_index, cass_health)
994
+ - Integration: Wire up SessionIndexer
995
+
996
+ 9. **T9: Dual-Mode Support** (3h)
997
+ - Files: `src/sessions.ts` (add mode switching)
998
+ - Config: Add mode: binary | inhouse | hybrid
999
+
1000
+ 10. **T10: Observability + Migration** (2h)
1001
+ - Files: Add Pino logging to all components
1002
+ - Validation: Run characterization tests in hybrid mode
1003
+
1004
+ **Total Estimate:** 28 hours (3.5 days)
1005
+
1006
+ ## Observability Hooks (ADR-005 Alignment)
1007
+
1008
+ ### Structured Logging (Pino)
1009
+
1010
+ All session indexing components use Pino logger from ADR-005:
1011
+
1012
+ ```typescript
1013
+ // logger.ts
1014
+ import pino from 'pino';
1015
+
1016
+ export const sessionLogger = pino({
1017
+ name: 'session-indexer',
1018
+ level: process.env.LOG_LEVEL || 'info',
1019
+ transport: {
1020
+ target: 'pino-pretty',
1021
+ options: {
1022
+ colorize: true,
1023
+ translateTime: 'HH:MM:ss',
1024
+ ignore: 'pid,hostname'
1025
+ }
1026
+ }
1027
+ });
1028
+
1029
+ // Usage in session-parser.ts
1030
+ sessionLogger.info({
1031
+ event: 'parse-start',
1032
+ source_path: filePath,
1033
+ agent_type: agentType
1034
+ });
1035
+
1036
+ sessionLogger.warn({
1037
+ event: 'malformed-line',
1038
+ source_path: filePath,
1039
+ line_number: lineNum,
1040
+ error: err.message
1041
+ });
1042
+
1043
+ sessionLogger.error({
1044
+ event: 'parse-failure',
1045
+ source_path: filePath,
1046
+ error: err,
1047
+ stack: err.stack
1048
+ });
1049
+ ```
1050
+
1051
+ ### Metrics
1052
+
1053
+ **Index Performance:**
1054
+ - `session.index.duration_ms` - Time to index one session
1055
+ - `session.index.message_count` - Messages indexed per session
1056
+ - `session.index.embedding_duration_ms` - Time spent on embeddings
1057
+
1058
+ **Search Performance:**
1059
+ - `session.search.duration_ms` - Search latency
1060
+ - `session.search.result_count` - Results returned
1061
+ - `session.search.embedding_enabled` - Whether embeddings were used
1062
+
1063
+ **Health Metrics:**
1064
+ - `session.health.stale_count` - Number of stale sessions
1065
+ - `session.health.total_sessions` - Total indexed sessions
1066
+ - `session.health.agent_breakdown` - Sessions by agent type
1067
+
1068
+ ### Tracing
1069
+
1070
+ Integrate with ADR-005 OpenTelemetry spans:
1071
+
1072
+ ```typescript
1073
+ import { trace } from '@opentelemetry/api';
1074
+
1075
+ const tracer = trace.getTracer('session-indexer');
1076
+
1077
+ async function indexSession(filePath: string) {
1078
+ return tracer.startActiveSpan('indexSession', async (span) => {
1079
+ span.setAttribute('source_path', filePath);
1080
+ span.setAttribute('agent_type', agentType);
1081
+
1082
+ try {
1083
+ const messages = await parseSession(filePath);
1084
+ span.setAttribute('message_count', messages.length);
1085
+
1086
+ const chunks = await chunkMessages(messages);
1087
+ span.setAttribute('chunk_count', chunks.length);
1088
+
1089
+ await embedAndStore(chunks);
1090
+ span.setStatus({ code: SpanStatusCode.OK });
1091
+ } catch (err) {
1092
+ span.recordException(err);
1093
+ span.setStatus({ code: SpanStatusCode.ERROR });
1094
+ throw err;
1095
+ } finally {
1096
+ span.end();
1097
+ }
1098
+ });
1099
+ }
1100
+ ```
1101
+
1102
+ ## Consequences
1103
+
1104
+ ### What Becomes Easier
1105
+
1106
+ 1. **Unified Query Interface** - One API for semantic memory + session search
1107
+ 2. **No External Binary** - Fewer dependencies, simpler installation
1108
+ 3. **Tighter Integration** - Swarm sessions auto-indexed, no export step
1109
+ 4. **Custom Enhancements** - Can add features without forking CASS (e.g., graph search)
1110
+ 5. **TDD-Friendly** - We control the test surface, can characterize behavior
1111
+ 6. **Observability** - Integrated with ADR-005 logging/metrics from day 1
1112
+
1113
+ ### What Becomes Harder
1114
+
1115
+ 1. **Maintenance Burden** - We own session parsing for 10+ agent formats
1116
+ 2. **Feature Parity** - Missing CASS features (TUI, multi-machine sync, encrypted formats)
1117
+ 3. **Performance Expectations** - Users may expect CASS-level performance (<60ms search)
1118
+ 4. **Agent Format Changes** - Need to track upstream session format changes
1119
+ 5. **Initial Migration** - 3.5 days of focused effort required
1120
+
1121
+ ### Risks & Mitigations
1122
+
1123
+ | Risk | Impact | Mitigation |
1124
+ |------|--------|------------|
1125
+ | Session format changes break parsing | High | Versioned parsers, graceful degradation |
1126
+ | Ollama unavailable → no embeddings | Medium | FTS5 fallback, queue for retry |
1127
+ | File watcher misses events | Medium | Periodic full scan (daily), staleness detection |
1128
+ | Search performance slower than CASS | Medium | Index optimization, edge n-grams for prefix search |
1129
+ | Users expect full CASS feature parity | Low | Clearly document Phase 1 scope, roadmap for Phase 2 |
1130
+
1131
+ ### Future Work (Out of Scope for Phase 1)
1132
+
1133
+ - **TUI** - Interactive terminal UI with syntax highlighting (CASS has this)
1134
+ - **Multi-machine sync** - SSH/rsync for remote session sources
1135
+ - **Encrypted formats** - ChatGPT v2/v3 with macOS keychain decryption
1136
+ - **Tantivy migration** - Replace FTS5 with Tantivy for <60ms search
1137
+ - **Vector hybrid search** - RRF fusion of BM25 + semantic embeddings
1138
+ - **Cloud agent connectors** - API integration for Claude Code, Gemini, Copilot
1139
+ - **Real-time collaboration** - Multi-agent session sharing via Swarm Mail
1140
+
1141
+ ## References
1142
+
1143
+ - **CASS Repository:** https://github.com/Dicklesworthstone/coding_agent_session_search
1144
+ - **ADR-005:** Swarm DevTools Observability (Pino logging, OpenTelemetry)
1145
+ - **Semantic Memory Docs:** `swarm-mail/README.md#semantic-memory`
1146
+ - **Session Formats Survey:** semantic-memory ID `42e210ae-f69f-47f9-995c-62f9a39ff7ec`
1147
+ - **Gap Analysis:** semantic-memory ID `319a7c67-9937-4f52-b3f5-31e06840b7ab`
1148
+
1149
+ ---
1150
+
1151
+ ## ASCII Art: The Inhousing Vision
1152
+
1153
+ ```
1154
+ 🔍 CASS Inhousing: Bridging the Gap
1155
+
1156
+
1157
+ BEFORE (External Binary)
1158
+ ┌────────────────────────────────────┐
1159
+ │ OpenCode Swarm Plugin │
1160
+ │ ├─ hive (work tracking) │
1161
+ │ ├─ swarm (coordination) │
1162
+ │ └─ semantic-memory (learning) │
1163
+ └────────────────────────────────────┘
1164
+
1165
+ │ shell exec
1166
+
1167
+ ┌────────────────────────────────────┐
1168
+ │ CASS Binary (Rust) │
1169
+ │ ├─ 20K LOC Tantivy indexer │
1170
+ │ ├─ 10 agent connectors │
1171
+ │ └─ TUI + Robot API │
1172
+ └────────────────────────────────────┘
1173
+
1174
+
1175
+ AFTER (Partial Inhousing)
1176
+ ┌────────────────────────────────────┐
1177
+ │ OpenCode Swarm Plugin │
1178
+ │ ├─ hive (work tracking) │
1179
+ │ ├─ swarm (coordination) │
1180
+ │ ├─ semantic-memory (learning) │
1181
+ │ └─ session-indexer ⭐ │
1182
+ │ ├─ file watcher │
1183
+ │ ├─ JSONL parsers (2 agents) │
1184
+ │ ├─ metadata schema │
1185
+ │ └─ MCP tools │
1186
+ └────────────────────────────────────┘
1187
+
1188
+ │ reuses 90%
1189
+
1190
+ ┌────────────────────────────────────┐
1191
+ │ Semantic Memory Infrastructure │
1192
+ │ ├─ libSQL + vector search │
1193
+ │ ├─ Ollama embeddings │
1194
+ │ ├─ FTS5 full-text │
1195
+ │ └─ confidence decay │
1196
+ └────────────────────────────────────┘
1197
+
1198
+
1199
+ THE GAP: 8 Thin Adapters (3.5 Days)
1200
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1201
+ 1. Session parsing (JSONL)
1202
+ 2. Chunking (message-level)
1203
+ 3. Metadata schema (agent_type, etc)
1204
+ 4. File watching (debounced)
1205
+ 5. Agent discovery (path mapping)
1206
+ 6. Staleness detection (mtime)
1207
+ 7. Pagination API (fields param)
1208
+ 8. Session viewer (line reader)
1209
+ ```
1210
+
1211
+ **The Vision:** Bring the best of CASS (session indexing, agent awareness) into our existing semantic-memory infrastructure. Focus on JSONL formats (OpenCode Swarm, Cursor) first. Iterate based on usage.
1212
+
1213
+ **The Bet:** 90% of CASS's value comes from session indexing, not the TUI or multi-machine sync. We can build that 90% in 3.5 days by reusing our existing infrastructure.
1214
+
1215
+ **The Payoff:** Unified query API, tighter integration, one less external dependency, full TDD coverage from day 1.