opencode-swarm-plugin 0.43.0 → 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.
- package/.turbo/turbo-build.log +2 -2
- package/CHANGELOG.md +31 -0
- package/bin/cass.characterization.test.ts +422 -0
- package/bin/swarm.test.ts +68 -0
- package/bin/swarm.ts +67 -0
- package/dist/contributor-tools.d.ts +42 -0
- package/dist/contributor-tools.d.ts.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +95 -2
- package/dist/plugin.js +95 -2
- package/dist/sessions/agent-discovery.d.ts +59 -0
- package/dist/sessions/agent-discovery.d.ts.map +1 -0
- package/dist/sessions/index.d.ts +10 -0
- package/dist/sessions/index.d.ts.map +1 -0
- package/docs/planning/ADR-010-cass-inhousing.md +1215 -0
- package/evals/fixtures/cass-baseline.ts +217 -0
- package/examples/plugin-wrapper-template.ts +89 -0
- package/package.json +1 -1
- package/src/contributor-tools.test.ts +133 -0
- package/src/contributor-tools.ts +201 -0
- package/src/index.ts +8 -3
- package/src/sessions/agent-discovery.test.ts +137 -0
- package/src/sessions/agent-discovery.ts +112 -0
- 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.
|