swarm-mail 1.6.0 → 1.6.2

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 (49) hide show
  1. package/README.md +953 -50
  2. package/dist/db/client.d.ts.map +1 -1
  3. package/dist/db/schema/streams.d.ts +490 -0
  4. package/dist/db/schema/streams.d.ts.map +1 -1
  5. package/dist/hive/index.d.ts +1 -1
  6. package/dist/hive/index.d.ts.map +1 -1
  7. package/dist/hive/queries-drizzle.d.ts +13 -0
  8. package/dist/hive/queries-drizzle.d.ts.map +1 -1
  9. package/dist/hive/queries.d.ts +18 -0
  10. package/dist/hive/queries.d.ts.map +1 -1
  11. package/dist/index.d.ts +9 -0
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +34815 -33256
  14. package/dist/memory/adapter.d.ts +3 -0
  15. package/dist/memory/adapter.d.ts.map +1 -1
  16. package/dist/memory/migrations.d.ts +19 -0
  17. package/dist/memory/migrations.d.ts.map +1 -1
  18. package/dist/sessions/chunk-processor.d.ts +104 -0
  19. package/dist/sessions/chunk-processor.d.ts.map +1 -0
  20. package/dist/sessions/file-watcher.d.ts +86 -0
  21. package/dist/sessions/file-watcher.d.ts.map +1 -0
  22. package/dist/sessions/index.d.ts +13 -0
  23. package/dist/sessions/index.d.ts.map +1 -0
  24. package/dist/sessions/pagination.d.ts +60 -0
  25. package/dist/sessions/pagination.d.ts.map +1 -0
  26. package/dist/sessions/session-indexer.d.ts +210 -0
  27. package/dist/sessions/session-indexer.d.ts.map +1 -0
  28. package/dist/sessions/session-parser.d.ts +88 -0
  29. package/dist/sessions/session-parser.d.ts.map +1 -0
  30. package/dist/sessions/session-viewer.d.ts +20 -0
  31. package/dist/sessions/session-viewer.d.ts.map +1 -0
  32. package/dist/sessions/staleness-detector.d.ts +58 -0
  33. package/dist/sessions/staleness-detector.d.ts.map +1 -0
  34. package/dist/streams/agent-mail.d.ts.map +1 -1
  35. package/dist/streams/decision-trace-store.d.ts +258 -0
  36. package/dist/streams/decision-trace-store.d.ts.map +1 -0
  37. package/dist/streams/durable-server.d.ts +12 -3
  38. package/dist/streams/durable-server.d.ts.map +1 -1
  39. package/dist/streams/events.d.ts +3479 -108
  40. package/dist/streams/events.d.ts.map +1 -1
  41. package/dist/streams/index.d.ts +1 -0
  42. package/dist/streams/index.d.ts.map +1 -1
  43. package/dist/streams/libsql-schema.d.ts +2 -0
  44. package/dist/streams/libsql-schema.d.ts.map +1 -1
  45. package/dist/streams/store.d.ts +4 -0
  46. package/dist/streams/store.d.ts.map +1 -1
  47. package/dist/streams/swarm-mail.d.ts +14 -0
  48. package/dist/streams/swarm-mail.d.ts.map +1 -1
  49. package/package.json +4 -3
package/README.md CHANGED
@@ -39,7 +39,7 @@ A TypeScript library providing:
39
39
  1. **Event Store** - Append-only log with automatic projection updates (agents, messages, file reservations)
40
40
  2. **Actor Primitives** - DurableMailbox, DurableLock, DurableCursor, DurableDeferred (Effect-TS based)
41
41
  3. **Hive** - Git-synced work item tracker (cells, epics, dependencies)
42
- 4. **Semantic Memory** - Vector embeddings for persistent agent learnings (Ollama + pgvector)
42
+ 4. **Semantic Memory** - Vector embeddings for persistent agent learnings (Ollama + libSQL native vector support via sqlite-vec)
43
43
 
44
44
  ```
45
45
  ┌─────────────────────────────────────────────────────────────┐
@@ -58,13 +58,98 @@ A TypeScript library providing:
58
58
  │ └── DurableDeferred - Distributed promise │
59
59
  │ │
60
60
  │ MEMORY │
61
- │ └── Semantic Memory - Vector embeddings (pgvector/Ollama) │
61
+ │ └── Semantic Memory - Vector embeddings (Ollama/sqlite-vec) │
62
62
  │ │
63
63
  │ STORAGE │
64
64
  │ └── libSQL (Embedded SQLite via Drizzle ORM) │
65
65
  └─────────────────────────────────────────────────────────────┘
66
66
  ```
67
67
 
68
+ ## Event Flow Architecture
69
+
70
+ Shows how agent actions flow from tool calls → libSQL events → CLI queries/dashboards.
71
+
72
+ ```
73
+ ┌──────────────────────────────────────────────────────────────────────────┐
74
+ │ EVENT FLOW: Agent → libSQL → CLI │
75
+ ├──────────────────────────────────────────────────────────────────────────┤
76
+ │ │
77
+ │ ╔═══════════════════╗ │
78
+ │ ║ AGENT (Worker) ║ Tool Calls via OpenCode MCP: │
79
+ │ ╚═══════════════════╝ ┌────────────────────────────────────┐ │
80
+ │ │ │ swarmmail_init() │ │
81
+ │ │ │ swarmmail_reserve(["src/auth.ts"]) │ │
82
+ │ │ │ swarm_progress(progress=50) │ │
83
+ │ │ │ swarm_complete(...) │ │
84
+ │ │ └────────────────────────────────────┘ │
85
+ │ ▼ │
86
+ │ ╔═══════════════════════════════════════════════════════════════╗ │
87
+ │ ║ ADAPTER LAYER (swarm-mail) ║ │
88
+ │ ║ packages/swarm-mail/src/adapter.ts ║ │
89
+ │ ╠═══════════════════════════════════════════════════════════════╣ │
90
+ │ ║ appendEvent(event: SwarmMailEvent) ║ │
91
+ │ ║ 1. Validate with Zod schemas (30+ event types) ║ │
92
+ │ ║ 2. Serialize event.data to JSON ║ │
93
+ │ ║ 3. INSERT into events table ║ │
94
+ │ ║ ║ │
95
+ │ ║ Query Helpers (read from projections): ║ │
96
+ │ ║ ├─ getInbox() → messages table ║ │
97
+ │ ║ ├─ getReservations() → reservations table ║ │
98
+ │ ║ └─ getSwarmContext() → swarm_contexts table ║ │
99
+ │ ╚═══════════════════════════════════════════════════════════════╝ │
100
+ │ │ │
101
+ │ ▼ │
102
+ │ ╔═══════════════════════════════════════════════════════════════╗ │
103
+ │ ║ libSQL DATABASE (SQLite embedded, no server) ║ │
104
+ │ ║ ~/.config/swarm-tools/libsql/<project-hash>/swarm.db ║ │
105
+ │ ╠═══════════════════════════════════════════════════════════════╣ │
106
+ │ ║ events (append-only log) ║ │
107
+ │ ║ ┌──────┬──────────┬───────────┬──────────┬──────────┐ ║ │
108
+ │ ║ │ id │ type │ timestamp │project_key│ data │ ║ │
109
+ │ ║ ├──────┼──────────┼───────────┼──────────┼──────────┤ ║ │
110
+ │ ║ │ 1 │agent_reg │1703001234 │/proj/path│{"..."} │ ║ │
111
+ │ ║ │ 2 │file_res │1703001240 │/proj/path│{"..."} │ ║ │
112
+ │ ║ │ 3 │task_start│1703001299 │/proj/path│{"..."} │ ║ │
113
+ │ ║ └──────┴──────────┴───────────┴──────────┴──────────┘ ║ │
114
+ │ ║ ║ │
115
+ │ ║ PROJECTIONS (auto-updated via triggers): ║ │
116
+ │ ║ ├─ agents ← agent_registered, agent_active ║ │
117
+ │ ║ ├─ messages ← message_sent ║ │
118
+ │ ║ ├─ reservations ← file_reserved, file_released ║ │
119
+ │ ║ ├─ swarm_contexts ← swarm_checkpointed ║ │
120
+ │ ║ └─ eval_records ← eval_captured, eval_scored ║ │
121
+ │ ╚═══════════════════════════════════════════════════════════════╝ │
122
+ │ │ │
123
+ │ ┌──────┼─────┬──────────┬──────────┬──────────┐ │
124
+ │ ▼ ▼ ▼ ▼ ▼ ▼ │
125
+ │ ┌────┐┌────┐┌─────────┐┌────────┐┌────────┐┌────────┐ │
126
+ │ │query││stats││dashboard││replay ││export ││log │ │
127
+ │ │(SQL)││(cnt)││ (TUI) ││(time) ││(JSONL) ││(tail) │ │
128
+ │ └────┘└────┘└─────────┘└────────┘└────────┘└────────┘ │
129
+ │ │ │ │ │ │ │ │
130
+ │ └─────┴───────┴──────────┴─────────┴─────────┘ │
131
+ │ ▼ │
132
+ │ ╔═══════════════════════════════════════════════════════════════╗ │
133
+ │ ║ CLI OUTPUT ║ │
134
+ │ ╠═══════════════════════════════════════════════════════════════╣ │
135
+ │ ║ 📊 Analytics 📈 Dashboards 🔍 Debugging ║ │
136
+ │ ║ ├─ Event counts ├─ Live progress ├─ Event replay ║ │
137
+ │ ║ ├─ Duration P95 ├─ Agent status ├─ Agent timeline ║ │
138
+ │ ║ ├─ Failure rate ├─ File locks ├─ Conflict detection ║ │
139
+ │ ║ └─ Conflict rate └─ Auto-refresh └─ Checkpoint history ║ │
140
+ │ ╚═══════════════════════════════════════════════════════════════╝ │
141
+ │ │
142
+ └──────────────────────────────────────────────────────────────────────────┘
143
+ ```
144
+
145
+ **Flow Summary:**
146
+
147
+ 1. **Agent Tool Call** → Agent calls `swarmmail_init()`, `swarm_progress()`, etc.
148
+ 2. **Adapter Layer** → Validates with Zod (30+ event types), serializes to JSON, appends to `events` table
149
+ 3. **libSQL Storage** → Events stored in append-only log, projections auto-updated via triggers
150
+ 4. **CLI Tools** → Query events/projections for analytics, monitoring, debugging
151
+ 5. **Observability** → Full audit trail, replay capabilities, real-time dashboards
152
+
68
153
  ## Install
69
154
 
70
155
  ```bash
@@ -165,26 +250,33 @@ await hive.closeCell(cell.id, "Completed: OAuth implemented");
165
250
 
166
251
  ### Semantic Memory
167
252
 
168
- Vector embeddings for persistent agent learnings:
253
+ Vector embeddings for persistent agent learnings. Uses **libSQL native vector support via sqlite-vec extension** + **Ollama** for embeddings. Includes **Wave 1-3 smart operations** (Mem0 pattern, auto-tagging, linking, entity extraction).
254
+
255
+ #### Basic Usage
169
256
 
170
257
  ```typescript
171
258
  import { createSemanticMemory } from "swarm-mail";
172
259
 
173
260
  const memory = await createSemanticMemory("/my/project");
174
261
 
175
- // Store a learning
262
+ // Simple store (backward compatible - always adds new)
263
+ const { id } = await memory.store(
264
+ "OAuth refresh tokens need 5min buffer before expiry to avoid race conditions"
265
+ );
266
+
267
+ // Store with manual metadata
176
268
  const { id } = await memory.store(
177
- "OAuth refresh tokens need 5min buffer before expiry to avoid race conditions",
269
+ "OAuth tokens need 5min buffer before expiry",
178
270
  { tags: "auth,tokens,debugging" }
179
271
  );
180
272
 
181
- // Search by meaning (vector similarity)
273
+ // Search by semantic similarity (vector search)
182
274
  const results = await memory.find("token refresh issues", { limit: 5 });
183
275
 
184
- // Get memory by ID
276
+ // Get specific memory
185
277
  const mem = await memory.get(id);
186
278
 
187
- // Validate (resets decay timer)
279
+ // Validate (resets 90-day decay timer)
188
280
  await memory.validate(id);
189
281
 
190
282
  // Check Ollama health
@@ -192,7 +284,210 @@ const health = await memory.checkHealth();
192
284
  // { ollama: true, model: "mxbai-embed-large" }
193
285
  ```
194
286
 
195
- > **Note:** Requires [Ollama](https://ollama.ai/) for vector embeddings. Falls back to full-text search if unavailable.
287
+ #### Wave 1-3 Smart Operations
288
+
289
+ **Smart Upsert (Mem0 Pattern)**
290
+
291
+ LLM analyzes new information and decides: ADD (new), UPDATE (refines), DELETE (contradicts), or NOOP (duplicate):
292
+
293
+ ```typescript
294
+ // LLM decides operation intelligently
295
+ const result = await memory.upsert(
296
+ "OAuth tokens need 5min buffer (changed from 3min)",
297
+ { useSmartOps: true }
298
+ );
299
+
300
+ console.log(result.operation); // "UPDATE" - refines existing memory
301
+ console.log(result.reason); // "Refines existing memory with updated timing"
302
+ console.log(result.id); // Memory ID (existing for UPDATE/DELETE/NOOP, new for ADD)
303
+
304
+ // Examples of each operation:
305
+ // ADD: Completely new information not in memory
306
+ // UPDATE: Refines/updates existing memory with additional detail
307
+ // DELETE: New info contradicts existing memory (supersedes it)
308
+ // NOOP: Duplicate - already stored
309
+ ```
310
+
311
+ **Auto-Tagging**
312
+
313
+ LLM extracts relevant tags from content automatically:
314
+
315
+ ```typescript
316
+ const { id, autoTags } = await memory.store(
317
+ "OAuth tokens need 5min buffer before expiry",
318
+ { autoTag: true }
319
+ );
320
+
321
+ console.log(autoTags);
322
+ // { tags: ["auth", "oauth", "tokens", "timing"], confidence: 0.85 }
323
+ ```
324
+
325
+ **Memory Linking (Zettelkasten Pattern)**
326
+
327
+ Auto-link to semantically related memories:
328
+
329
+ ```typescript
330
+ const { id, links } = await memory.store(
331
+ "Token refresh race condition fixed in auth service",
332
+ { autoLink: true }
333
+ );
334
+
335
+ console.log(links);
336
+ // [{ memory_id: "mem-abc123", link_type: "related", score: 0.82 }]
337
+
338
+ // Query linked memories
339
+ const related = await memory.getLinkedMemories("mem-abc123", "related");
340
+ ```
341
+
342
+ **Entity Extraction (A-MEM Pattern)**
343
+
344
+ Build knowledge graph from natural language automatically:
345
+
346
+ ```typescript
347
+ const { id, entities } = await memory.store(
348
+ "Joel prefers TypeScript for Next.js projects",
349
+ { extractEntities: true }
350
+ );
351
+
352
+ console.log(entities);
353
+ // {
354
+ // entities: [
355
+ // { name: "Joel", type: "person" },
356
+ // { name: "TypeScript", type: "technology" },
357
+ // { name: "Next.js", type: "technology" }
358
+ // ],
359
+ // relationships: [
360
+ // { from: "Joel", to: "TypeScript", type: "prefers" }
361
+ // ]
362
+ // }
363
+
364
+ // Query by entity
365
+ const joelMemories = await memory.findByEntity("Joel", "person");
366
+
367
+ // Get knowledge graph for a memory
368
+ const graph = await memory.getKnowledgeGraph("mem-abc123");
369
+ // { entities: [...], relationships: [...] }
370
+ ```
371
+
372
+ **Combine All Smart Features**
373
+
374
+ ```typescript
375
+ const result = await memory.store(
376
+ "OAuth tokens need 5min buffer before expiry to avoid race conditions",
377
+ {
378
+ autoTag: true, // LLM extracts tags
379
+ autoLink: true, // Links to related memories
380
+ extractEntities: true // Builds knowledge graph
381
+ }
382
+ );
383
+
384
+ // Returns: { id, autoTags, links, entities }
385
+ ```
386
+
387
+ #### Temporal Queries (Wave 2)
388
+
389
+ Query memories valid at specific timestamps for time-travel debugging:
390
+
391
+ ```typescript
392
+ // Find memories valid on January 1, 2024
393
+ const pastMemories = await memory.findValidAt(
394
+ "authentication patterns",
395
+ new Date("2024-01-01")
396
+ );
397
+
398
+ // Track supersession chains (version history)
399
+ const chain = await memory.getSupersessionChain("mem-v1");
400
+ // Returns: [mem-v1, mem-v2, mem-v3] (chronological evolution)
401
+
402
+ // Mark memory as superseded (when updating)
403
+ await memory.supersede("mem-old-auth", "mem-new-auth");
404
+ ```
405
+
406
+ #### Schema Extensions (Wave 1-3)
407
+
408
+ New tables and columns added for smart operations:
409
+
410
+ **Tables:**
411
+ - `memory_links` - Semantic relationships between memories (Zettelkasten)
412
+ - `entities` - Extracted entities (people, places, concepts)
413
+ - `relationships` - Entity relationships (subject-predicate-object triples)
414
+ - `memory_entities` - Join table linking memories to entities
415
+
416
+ **Columns:**
417
+ - `memories.valid_from` - Temporal validity start (UTC timestamp)
418
+ - `memories.valid_until` - Temporal validity end (NULL = current)
419
+ - `memories.superseded_by` - Points to newer version (version chains)
420
+ - `memories.auto_tags` - JSON array of LLM-extracted tags
421
+ - `memories.keywords` - Searchable keyword index
422
+
423
+ #### Graceful Degradation
424
+
425
+ All smart operations use **fallback heuristics** if LLM/Ollama unavailable:
426
+
427
+ - **Smart ops**: Falls back to simple ADD operation
428
+ - **Auto-tagging**: Returns `undefined` (no auto-tags added)
429
+ - **Auto-linking**: Returns `undefined` (no links created)
430
+ - **Entity extraction**: Returns empty `entities`/`relationships` arrays
431
+ - **Vector search**: Falls back to full-text search (FTS5)
432
+
433
+ **No crashes, no exceptions** - degraded functionality, not broken functionality.
434
+
435
+ #### ID Prefix Convention
436
+
437
+ Memory IDs use `mem-` prefix (not `mem_`):
438
+
439
+ ```typescript
440
+ const { id } = await memory.store("Content");
441
+ console.log(id); // "mem-abc123def456" (16 hex chars after prefix)
442
+ ```
443
+
444
+ #### Wave 1-3 Service Exports
445
+
446
+ For advanced use cases, you can access the underlying services directly:
447
+
448
+ ```typescript
449
+ import {
450
+ // Smart Operations (Mem0 pattern)
451
+ analyzeMemoryOperation,
452
+
453
+ // Auto-tagging
454
+ generateTags,
455
+
456
+ // Memory Linking (Zettelkasten)
457
+ autoLinkMemory,
458
+ createLink,
459
+ findRelatedMemories,
460
+
461
+ // Entity Extraction (A-MEM)
462
+ extractEntitiesAndRelationships,
463
+ storeEntities,
464
+ } from "swarm-mail";
465
+
466
+ // Example: Manual smart operation
467
+ const analysis = await analyzeMemoryOperation(
468
+ "OAuth tokens need 5min buffer",
469
+ existingMemories,
470
+ aiGatewayKey
471
+ );
472
+ console.log(analysis.operation); // "ADD" | "UPDATE" | "DELETE" | "NOOP"
473
+
474
+ // Example: Manual tag generation
475
+ const tags = await generateTags(
476
+ "OAuth tokens need 5min buffer before expiry",
477
+ aiGatewayKey
478
+ );
479
+ console.log(tags); // { tags: ["auth", "oauth", "tokens"], confidence: 0.85 }
480
+
481
+ // Example: Manual entity extraction
482
+ const extracted = await extractEntitiesAndRelationships(
483
+ "Joel prefers TypeScript for Next.js projects",
484
+ aiGatewayKey
485
+ );
486
+ console.log(extracted.entities); // [{ name: "Joel", type: "person" }, ...]
487
+ console.log(extracted.relationships); // [{ from: "Joel", to: "TypeScript", type: "prefers" }]
488
+ ```
489
+
490
+ > **Note:** Requires [Ollama](https://ollama.ai/) for vector embeddings and smart operations. Falls back to full-text search if unavailable.
196
491
  >
197
492
  > ```bash
198
493
  > ollama pull mxbai-embed-large
@@ -225,54 +520,549 @@ await swarmMail.appendEvent({
225
520
  });
226
521
  ```
227
522
 
228
- ## Event Types
523
+ ## Event Schema
524
+
525
+ All swarm coordination is recorded as immutable events in libSQL. Events are **append-only** - nothing is deleted, everything is auditable.
526
+
527
+ ### Event Store Table
528
+
529
+ ```sql
530
+ CREATE TABLE events (
531
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
532
+ type TEXT NOT NULL, -- Event type discriminator
533
+ project_key TEXT NOT NULL, -- Project path (for multi-project isolation)
534
+ timestamp INTEGER NOT NULL, -- Unix ms
535
+ sequence INTEGER GENERATED ALWAYS AS (id) STORED,
536
+ data TEXT NOT NULL, -- JSON payload (event-specific fields)
537
+ created_at TEXT DEFAULT (datetime('now'))
538
+ );
539
+
540
+ -- Fast queries via composite indexes
541
+ CREATE INDEX idx_events_project_key ON events(project_key);
542
+ CREATE INDEX idx_events_type ON events(type);
543
+ CREATE INDEX idx_events_timestamp ON events(timestamp);
544
+ CREATE INDEX idx_events_project_type ON events(project_key, type);
545
+ ```
546
+
547
+ ### Event Types
548
+
549
+ All events extend `BaseEventSchema` with common fields:
550
+ ```typescript
551
+ {
552
+ id?: number; // Auto-generated
553
+ type: string; // Event discriminator
554
+ project_key: string; // Project path
555
+ timestamp: number; // Unix ms
556
+ sequence?: number; // Auto-generated ordering
557
+ }
558
+ ```
559
+
560
+ **Agent Lifecycle (2 types):**
561
+ ```typescript
562
+ { type: "agent_registered"; agent_name: string; program?: string; model?: string; task_description?: string }
563
+ { type: "agent_active"; agent_name: string }
564
+ ```
565
+
566
+ **Messages (5 types):**
567
+ ```typescript
568
+ { type: "message_sent"; message_id?: number; from_agent: string; to_agents: string[]; subject: string; body: string; thread_id?: string; importance?: "low" | "normal" | "high" | "urgent"; ack_required?: boolean; epic_id?: string; bead_id?: string; message_type?: "progress" | "blocked" | "question" | "status" | "general"; body_length?: number; recipient_count?: number; is_broadcast?: boolean }
569
+ { type: "message_read"; message_id: number; agent_name: string }
570
+ { type: "message_acked"; message_id: number; agent_name: string }
571
+ { type: "thread_created"; thread_id: string; epic_id?: string; initial_subject: string; creator_agent: string }
572
+ { type: "thread_activity"; thread_id: string; message_count: number; participant_count: number; last_message_agent: string; has_unread: boolean }
573
+ ```
574
+
575
+ **File Reservations (3 types):**
576
+ ```typescript
577
+ { type: "file_reserved"; reservation_id?: number; agent_name: string; paths: string[]; reason?: string; exclusive?: boolean; ttl_seconds?: number; expires_at: number; lock_holder_ids?: string[]; epic_id?: string; bead_id?: string; file_count?: number; is_retry?: boolean; conflict_agent?: string }
578
+ { type: "file_released"; agent_name: string; paths?: string[]; reservation_ids?: number[]; lock_holder_ids?: string[]; epic_id?: string; bead_id?: string; file_count?: number; hold_duration_ms?: number; files_modified?: number }
579
+ { type: "file_conflict"; requesting_agent: string; holding_agent: string; paths: string[]; epic_id?: string; bead_id?: string; resolution?: "wait" | "force" | "abort" }
580
+ ```
581
+
582
+ **Task Tracking (4 types):**
583
+ ```typescript
584
+ { type: "task_started"; agent_name: string; bead_id: string; epic_id?: string }
585
+ { type: "task_progress"; agent_name: string; bead_id: string; progress_percent?: number; message?: string; files_touched?: string[] }
586
+ { type: "task_completed"; agent_name: string; bead_id: string; summary: string; files_touched?: string[]; success: boolean }
587
+ { type: "task_blocked"; agent_name: string; bead_id: string; reason: string }
588
+ ```
589
+
590
+ **Swarm Coordination (8 types):**
591
+ ```typescript
592
+ { type: "swarm_started"; epic_id: string; epic_title: string; strategy: "file-based" | "feature-based" | "risk-based"; subtask_count: number; total_files: number; coordinator_agent: string }
593
+ { type: "worker_spawned"; epic_id: string; bead_id: string; worker_agent: string; subtask_title: string; files_assigned: string[]; spawn_order: number; is_parallel: boolean }
594
+ { type: "worker_completed"; epic_id: string; bead_id: string; worker_agent: string; success: boolean; duration_ms: number; files_touched: string[]; error_message?: string }
595
+ { type: "review_started"; epic_id: string; bead_id: string; attempt: number }
596
+ { type: "review_completed"; epic_id: string; bead_id: string; status: "approved" | "needs_changes" | "blocked"; attempt: number; duration_ms?: number }
597
+ { type: "swarm_completed"; epic_id: string; epic_title: string; success: boolean; total_duration_ms: number; subtasks_completed: number; subtasks_failed: number; total_files_touched: string[] }
598
+ { type: "decomposition_generated"; epic_id: string; task: string; context?: string; strategy: "file-based" | "feature-based" | "risk-based"; epic_title: string; subtasks: Array<{ title: string; files: string[]; priority?: number }>; recovery_context?: object }
599
+ { type: "subtask_outcome"; epic_id: string; bead_id: string; planned_files: string[]; actual_files: string[]; duration_ms: number; error_count?: number; retry_count?: number; success: boolean; scope_violation?: boolean; violation_files?: string[] }
600
+ ```
601
+
602
+ **Checkpoints & Recovery (4 types):**
603
+ ```typescript
604
+ { type: "swarm_checkpointed"; epic_id: string; bead_id: string; strategy: "file-based" | "feature-based" | "risk-based"; files: string[]; dependencies: string[]; directives: object; recovery: object; checkpoint_size_bytes?: number; trigger?: "manual" | "auto" | "progress" | "error"; context_tokens_before?: number; context_tokens_after?: number }
605
+ { type: "swarm_recovered"; epic_id: string; bead_id: string; recovered_from_checkpoint: number; recovery_duration_ms?: number; checkpoint_age_ms?: number; files_restored?: string[]; context_restored_tokens?: number }
606
+ { type: "checkpoint_created"; epic_id: string; bead_id: string; agent_name: string; checkpoint_id: string; trigger: "manual" | "auto" | "progress" | "error"; progress_percent: number; files_snapshot: string[] }
607
+ { type: "context_compacted"; epic_id?: string; bead_id?: string; agent_name: string; tokens_before: number; tokens_after: number; compression_ratio: number; summary_length: number }
608
+ ```
609
+
610
+ **Validation & Learning (4 types):**
611
+ ```typescript
612
+ { type: "validation_started"; epic_id: string; swarm_id: string; started_at: number }
613
+ { type: "validation_issue"; epic_id: string; severity: "error" | "warning" | "info"; category: "schema_mismatch" | "missing_event" | "undefined_value" | "dashboard_render" | "websocket_delivery"; message: string; location?: object }
614
+ { type: "validation_completed"; epic_id: string; swarm_id: string; passed: boolean; issue_count: number; duration_ms: number }
615
+ { type: "human_feedback"; epic_id: string; accepted: boolean; modified?: boolean; notes?: string }
616
+ ```
617
+
618
+ **Total: 30+ event types** - Full Zod schemas in [src/streams/events.ts](src/streams/events.ts)
619
+
620
+ ## libSQL Query Examples
621
+
622
+ All state is derived from events via **projections** (materialized views). Query the event store directly or use projections for common patterns.
623
+
624
+ ### Agent Queries
625
+
626
+ ```typescript
627
+ // Who's registered in this project?
628
+ const agents = await swarmMail.getAgents();
629
+ // SELECT * FROM agents WHERE project_key = ? ORDER BY registered_at
630
+
631
+ // Get specific agent details
632
+ const agent = await swarmMail.getAgent("WorkerBee");
633
+ // SELECT * FROM agents WHERE project_key = ? AND name = ?
634
+
635
+ // Debug agent activity
636
+ const debug = await swarmMail.debugAgent("WorkerBee");
637
+ // Returns: { agent, messages_sent, messages_received, reservations, recent_events }
638
+ ```
639
+
640
+ **Raw SQL examples:**
641
+
642
+ ```sql
643
+ -- All agents in project
644
+ SELECT name, program, model, task_description, registered_at
645
+ FROM agents
646
+ WHERE project_key = '/path/to/project'
647
+ ORDER BY last_active_at DESC;
648
+
649
+ -- Agents active in last hour
650
+ SELECT name, last_active_at
651
+ FROM agents
652
+ WHERE project_key = '/path/to/project'
653
+ AND last_active_at > (strftime('%s', 'now') - 3600) * 1000;
654
+ ```
655
+
656
+ ### Message Queries
657
+
658
+ ```typescript
659
+ // Get inbox (materialized view)
660
+ const messages = await swarmMail.getInbox("WorkerBee", {
661
+ limit: 5,
662
+ unreadOnly: true
663
+ });
664
+ // SELECT * FROM messages WHERE ... ORDER BY sent_at DESC LIMIT 5
665
+
666
+ // Get thread (all messages with same thread_id)
667
+ const thread = await swarmMail.getThread("epic-123");
668
+ // SELECT * FROM messages WHERE thread_id = ? ORDER BY sent_at
669
+
670
+ // Mark as read
671
+ await swarmMail.appendEvent({
672
+ type: "message_read",
673
+ message_id: 42,
674
+ agent_name: "WorkerBee",
675
+ timestamp: Date.now(),
676
+ });
677
+ ```
678
+
679
+ **Raw SQL examples:**
680
+
681
+ ```sql
682
+ -- Unread messages for agent
683
+ SELECT m.id, m.subject, m.from_agent, m.sent_at, m.importance
684
+ FROM messages m
685
+ LEFT JOIN message_reads mr
686
+ ON mr.message_id = m.id AND mr.agent_name = 'WorkerBee'
687
+ WHERE m.project_key = '/path/to/project'
688
+ AND EXISTS (
689
+ SELECT 1 FROM message_recipients
690
+ WHERE message_id = m.id AND recipient_name = 'WorkerBee'
691
+ )
692
+ AND mr.id IS NULL
693
+ ORDER BY m.sent_at DESC;
694
+
695
+ -- Thread view with participants
696
+ SELECT m.id, m.subject, m.from_agent, m.body, m.sent_at,
697
+ GROUP_CONCAT(mr.recipient_name) as recipients
698
+ FROM messages m
699
+ LEFT JOIN message_recipients mr ON mr.message_id = m.id
700
+ WHERE m.thread_id = 'epic-123'
701
+ GROUP BY m.id
702
+ ORDER BY m.sent_at;
703
+
704
+ -- Message traffic by importance
705
+ SELECT importance, COUNT(*) as count
706
+ FROM messages
707
+ WHERE project_key = '/path/to/project'
708
+ AND sent_at > (strftime('%s', 'now') - 86400) * 1000 -- last 24h
709
+ GROUP BY importance;
710
+ ```
711
+
712
+ ### File Reservation Queries
713
+
714
+ ```typescript
715
+ // Check for conflicts before reserving
716
+ const conflicts = await swarmMail.checkConflicts(
717
+ ["src/auth.ts", "src/db.ts"],
718
+ "WorkerBee"
719
+ );
720
+ // Returns: [{ path, holder, expires_at, exclusive }]
721
+
722
+ // Get all active reservations
723
+ const reservations = await swarmMail.getReservations();
724
+ // SELECT * FROM reservations WHERE expires_at > ? ORDER BY created_at
725
+
726
+ // Get agent's reservations
727
+ const myReservations = await swarmMail.getReservationsForAgent("WorkerBee");
728
+ // SELECT * FROM reservations WHERE agent_name = ? AND expires_at > ?
729
+ ```
730
+
731
+ **Raw SQL examples:**
732
+
733
+ ```sql
734
+ -- Active file locks
735
+ SELECT agent_name, path_pattern, exclusive,
736
+ datetime(created_at/1000, 'unixepoch') as locked_at,
737
+ datetime(expires_at/1000, 'unixepoch') as expires
738
+ FROM reservations
739
+ WHERE project_key = '/path/to/project'
740
+ AND expires_at > (strftime('%s', 'now') * 1000)
741
+ ORDER BY created_at;
742
+
743
+ -- Conflict detection (who holds this path?)
744
+ SELECT agent_name, exclusive, expires_at
745
+ FROM reservations
746
+ WHERE project_key = '/path/to/project'
747
+ AND path_pattern LIKE '%src/auth.ts%'
748
+ AND expires_at > (strftime('%s', 'now') * 1000);
749
+
750
+ -- Expired reservations (cleanup candidates)
751
+ SELECT agent_name, path_pattern,
752
+ (strftime('%s', 'now') * 1000 - expires_at) / 1000 as expired_seconds_ago
753
+ FROM reservations
754
+ WHERE project_key = '/path/to/project'
755
+ AND expires_at < (strftime('%s', 'now') * 1000)
756
+ ORDER BY expires_at;
757
+
758
+ -- Reservation timeline (who locked what when?)
759
+ SELECT
760
+ datetime(timestamp/1000, 'unixepoch') as time,
761
+ json_extract(data, '$.agent_name') as agent,
762
+ json_extract(data, '$.paths') as paths,
763
+ json_extract(data, '$.exclusive') as exclusive,
764
+ CASE type
765
+ WHEN 'file_reserved' THEN '🔒 LOCK'
766
+ WHEN 'file_released' THEN '🔓 RELEASE'
767
+ END as action
768
+ FROM events
769
+ WHERE project_key = '/path/to/project'
770
+ AND type IN ('file_reserved', 'file_released')
771
+ ORDER BY timestamp DESC
772
+ LIMIT 20;
773
+ ```
774
+
775
+ ### Task Progress Queries
776
+
777
+ ```typescript
778
+ // Get swarm checkpoint for recovery
779
+ const context = await swarmMail.getSwarmContext("epic-123");
780
+ // SELECT * FROM swarm_contexts WHERE epic_id = ? ORDER BY version DESC LIMIT 1
781
+
782
+ // Query events for timeline
783
+ const events = await swarmMail.getEvents({
784
+ limit: 100,
785
+ after: lastSequence
786
+ });
787
+ // SELECT * FROM events WHERE sequence > ? ORDER BY sequence LIMIT 100
788
+ ```
789
+
790
+ **Raw SQL examples:**
791
+
792
+ ```sql
793
+ -- Task timeline (start → progress → complete)
794
+ SELECT
795
+ datetime(timestamp/1000, 'unixepoch') as time,
796
+ type,
797
+ json_extract(data, '$.agent_name') as agent,
798
+ json_extract(data, '$.bead_id') as task,
799
+ json_extract(data, '$.progress_percent') as progress,
800
+ json_extract(data, '$.message') as status
801
+ FROM events
802
+ WHERE project_key = '/path/to/project'
803
+ AND type IN ('task_started', 'task_progress', 'task_completed', 'task_blocked')
804
+ AND json_extract(data, '$.epic_id') = 'epic-123'
805
+ ORDER BY timestamp;
806
+
807
+ -- Task outcomes (success vs failure)
808
+ SELECT
809
+ json_extract(data, '$.bead_id') as task,
810
+ json_extract(data, '$.success') as success,
811
+ json_extract(data, '$.duration_ms') as duration,
812
+ json_extract(data, '$.error_count') as errors,
813
+ datetime(timestamp/1000, 'unixepoch') as completed_at
814
+ FROM events
815
+ WHERE type = 'task_completed'
816
+ AND json_extract(data, '$.epic_id') = 'epic-123'
817
+ ORDER BY timestamp;
818
+
819
+ -- Checkpoint history (resume points)
820
+ SELECT version, progress,
821
+ datetime(created_at/1000, 'unixepoch') as checkpoint_time,
822
+ json_extract(state, '$.files_touched') as files
823
+ FROM swarm_contexts
824
+ WHERE epic_id = 'epic-123'
825
+ ORDER BY version DESC;
826
+ ```
827
+
828
+ ### Analytics Queries
829
+
830
+ **Four Golden Signals** for observability:
831
+
832
+ ```sql
833
+ -- 1. LATENCY - Task duration distribution
834
+ SELECT
835
+ CAST(json_extract(data, '$.duration_ms') / 1000.0 AS INT) as seconds,
836
+ COUNT(*) as tasks,
837
+ ROUND(AVG(json_extract(data, '$.duration_ms')), 0) as avg_ms
838
+ FROM events
839
+ WHERE type = 'task_completed'
840
+ AND json_extract(data, '$.success') = 1
841
+ GROUP BY seconds
842
+ ORDER BY seconds;
843
+
844
+ -- 2. TRAFFIC - Events per hour
845
+ SELECT
846
+ strftime('%Y-%m-%d %H:00', datetime(timestamp/1000, 'unixepoch')) as hour,
847
+ type,
848
+ COUNT(*) as count
849
+ FROM events
850
+ WHERE timestamp > (strftime('%s', 'now') - 86400) * 1000 -- last 24h
851
+ GROUP BY hour, type
852
+ ORDER BY hour DESC, count DESC;
853
+
854
+ -- 3. ERRORS - Failure analysis
855
+ SELECT
856
+ json_extract(data, '$.bead_id') as task,
857
+ json_extract(data, '$.error_count') as errors,
858
+ json_extract(data, '$.retry_count') as retries,
859
+ json_extract(data, '$.files_touched') as files,
860
+ datetime(timestamp/1000, 'unixepoch') as failed_at
861
+ FROM events
862
+ WHERE type = 'task_completed'
863
+ AND json_extract(data, '$.success') = 0
864
+ ORDER BY timestamp DESC
865
+ LIMIT 10;
866
+
867
+ -- 4. SATURATION - File contention
868
+ SELECT
869
+ json_extract(data, '$.paths') as file_path,
870
+ COUNT(DISTINCT json_extract(data, '$.agent_name')) as agent_count,
871
+ COUNT(*) as reservation_attempts,
872
+ GROUP_CONCAT(json_extract(data, '$.agent_name')) as competing_agents
873
+ FROM events
874
+ WHERE type = 'file_reserved'
875
+ AND timestamp > (strftime('%s', 'now') - 3600) * 1000 -- last hour
876
+ GROUP BY file_path
877
+ HAVING agent_count > 1
878
+ ORDER BY reservation_attempts DESC;
879
+ ```
880
+
881
+ ### Debugging Queries
882
+
883
+ ```sql
884
+ -- Agent activity timeline (comprehensive view)
885
+ SELECT
886
+ datetime(timestamp/1000, 'unixepoch') as time,
887
+ type,
888
+ CASE
889
+ WHEN type = 'agent_registered' THEN 'Registered: ' || json_extract(data, '$.task_description')
890
+ WHEN type = 'message_sent' THEN 'Sent: ' || json_extract(data, '$.subject')
891
+ WHEN type = 'file_reserved' THEN 'Locked: ' || json_extract(data, '$.paths')
892
+ WHEN type = 'file_released' THEN 'Released: ' || json_extract(data, '$.file_count') || ' files'
893
+ WHEN type = 'task_started' THEN 'Started: ' || json_extract(data, '$.bead_id')
894
+ WHEN type = 'task_progress' THEN 'Progress: ' || json_extract(data, '$.progress_percent') || '%'
895
+ WHEN type = 'task_completed' THEN 'Completed: ' || json_extract(data, '$.bead_id') || ' (' || CASE WHEN json_extract(data, '$.success') = 1 THEN 'success' ELSE 'failed' END || ')'
896
+ WHEN type = 'task_blocked' THEN 'Blocked: ' || json_extract(data, '$.reason')
897
+ ELSE type
898
+ END as activity
899
+ FROM events
900
+ WHERE json_extract(data, '$.agent_name') = 'WorkerBee'
901
+ ORDER BY timestamp DESC
902
+ LIMIT 50;
903
+
904
+ -- Find stuck tasks (started but not completed)
905
+ WITH task_events AS (
906
+ SELECT
907
+ json_extract(data, '$.bead_id') as task_id,
908
+ json_extract(data, '$.agent_name') as agent,
909
+ MIN(CASE WHEN type = 'task_started' THEN timestamp END) as started_at,
910
+ MAX(CASE WHEN type IN ('task_completed', 'task_blocked') THEN timestamp END) as ended_at
911
+ FROM events
912
+ WHERE type IN ('task_started', 'task_completed', 'task_blocked')
913
+ GROUP BY task_id, agent
914
+ )
915
+ SELECT
916
+ task_id,
917
+ agent,
918
+ datetime(started_at/1000, 'unixepoch') as started,
919
+ ROUND((strftime('%s', 'now') * 1000 - started_at) / 60000.0, 1) as minutes_stuck
920
+ FROM task_events
921
+ WHERE ended_at IS NULL
922
+ ORDER BY started_at DESC;
923
+
924
+ -- Message response times (high/urgent only)
925
+ WITH message_timings AS (
926
+ SELECT
927
+ json_extract(sent.data, '$.message_id') as msg_id,
928
+ json_extract(sent.data, '$.subject') as subject,
929
+ json_extract(sent.data, '$.from_agent') as sender,
930
+ json_extract(sent.data, '$.importance') as importance,
931
+ sent.timestamp as sent_at,
932
+ read.timestamp as read_at
933
+ FROM events sent
934
+ LEFT JOIN events read
935
+ ON read.type = 'message_read'
936
+ AND json_extract(read.data, '$.message_id') = json_extract(sent.data, '$.message_id')
937
+ WHERE sent.type = 'message_sent'
938
+ AND json_extract(sent.data, '$.importance') IN ('high', 'urgent')
939
+ )
940
+ SELECT
941
+ msg_id,
942
+ subject,
943
+ sender,
944
+ importance,
945
+ datetime(sent_at/1000, 'unixepoch') as sent,
946
+ CASE
947
+ WHEN read_at IS NULL THEN 'UNREAD'
948
+ ELSE ROUND((read_at - sent_at) / 1000.0, 1) || 's'
949
+ END as response_time
950
+ FROM message_timings
951
+ ORDER BY sent_at DESC
952
+ LIMIT 20;
953
+
954
+ -- Swarm execution summary (epic overview)
955
+ WITH epic_events AS (
956
+ SELECT
957
+ json_extract(data, '$.epic_id') as epic_id,
958
+ type,
959
+ timestamp,
960
+ data
961
+ FROM events
962
+ WHERE json_extract(data, '$.epic_id') = 'mjmas3zxlmg'
963
+ )
964
+ SELECT
965
+ 'Epic' as metric,
966
+ json_extract((SELECT data FROM epic_events WHERE type = 'swarm_started' LIMIT 1), '$.epic_title') as value
967
+ UNION ALL
968
+ SELECT
969
+ 'Strategy',
970
+ json_extract((SELECT data FROM epic_events WHERE type = 'swarm_started' LIMIT 1), '$.strategy')
971
+ UNION ALL
972
+ SELECT
973
+ 'Workers Spawned',
974
+ CAST(COUNT(*) AS TEXT)
975
+ FROM epic_events WHERE type = 'worker_spawned'
976
+ UNION ALL
977
+ SELECT
978
+ 'Tasks Completed',
979
+ CAST(COUNT(*) AS TEXT)
980
+ FROM epic_events WHERE type = 'worker_completed' AND json_extract(data, '$.success') = 1
981
+ UNION ALL
982
+ SELECT
983
+ 'Tasks Failed',
984
+ CAST(COUNT(*) AS TEXT)
985
+ FROM epic_events WHERE type = 'worker_completed' AND json_extract(data, '$.success') = 0
986
+ UNION ALL
987
+ SELECT
988
+ 'File Conflicts',
989
+ CAST(COUNT(*) AS TEXT)
990
+ FROM epic_events WHERE type = 'file_conflict'
991
+ UNION ALL
992
+ SELECT
993
+ 'Checkpoints',
994
+ CAST(COUNT(*) AS TEXT)
995
+ FROM epic_events WHERE type = 'checkpoint_created';
996
+
997
+ -- Worker performance comparison
998
+ SELECT
999
+ json_extract(data, '$.worker_agent') as worker,
1000
+ COUNT(*) as tasks_completed,
1001
+ SUM(CASE WHEN json_extract(data, '$.success') = 1 THEN 1 ELSE 0 END) as successful,
1002
+ ROUND(AVG(json_extract(data, '$.duration_ms')) / 1000.0, 1) as avg_duration_sec,
1003
+ ROUND(AVG(CAST(json_length(json_extract(data, '$.files_touched')) AS REAL)), 1) as avg_files_touched
1004
+ FROM events
1005
+ WHERE type = 'worker_completed'
1006
+ AND json_extract(data, '$.epic_id') = 'mjmas3zxlmg'
1007
+ GROUP BY worker
1008
+ ORDER BY successful DESC, avg_duration_sec ASC;
1009
+
1010
+ ## Dashboard Data Layer (Programmatic Access)
1011
+
1012
+ For building custom dashboards or monitoring tools, use the dashboard data layer from `opencode-swarm-plugin`:
229
1013
 
230
1014
  ```typescript
231
- type SwarmMailEvent =
232
- | { type: "agent_registered"; agent_name: string; task_description?: string }
233
- | {
234
- type: "message_sent";
235
- from: string;
236
- to: string[];
237
- subject: string;
238
- body: string;
239
- thread_id?: string;
240
- }
241
- | { type: "message_read"; message_id: number; agent_name: string }
242
- | {
243
- type: "file_reserved";
244
- agent_name: string;
245
- paths: string[];
246
- exclusive: boolean;
247
- reason?: string;
248
- }
249
- | { type: "file_released"; agent_name: string; paths: string[] }
250
- | {
251
- type: "swarm_checkpointed";
252
- epic_id: string;
253
- progress: number;
254
- state: object;
255
- }
256
- | { type: "decomposition_generated"; epic_id: string; subtasks: object[] }
257
- | {
258
- type: "subtask_outcome";
259
- bead_id: string;
260
- success: boolean;
261
- duration_ms: number;
262
- };
1015
+ import {
1016
+ getWorkerStatus,
1017
+ getSubtaskProgress,
1018
+ getFileLocks,
1019
+ getRecentMessages,
1020
+ getEpicList
1021
+ } from "opencode-swarm-plugin";
1022
+ import { getSwarmMailLibSQL } from "swarm-mail";
1023
+
1024
+ const db = await getSwarmMailLibSQL("/my/project");
1025
+
1026
+ // Get worker statuses (idle/working/blocked)
1027
+ const workers = await getWorkerStatus(db, { project_key: "/my/project" });
1028
+ // Returns: [{ agent_name: "Worker1", status: "working", current_task: "bd-123", last_activity: "2025-12-26T..." }]
1029
+
1030
+ // Get subtask progress for an epic
1031
+ const progress = await getSubtaskProgress(db, "mjmas3zxlmg");
1032
+ // Returns: [{ bead_id: "bd-123", title: "Add auth", status: "in_progress", progress_percent: 50 }]
1033
+
1034
+ // Get active file locks
1035
+ const locks = await getFileLocks(db);
1036
+ // Returns: [{ path: "src/auth.ts", agent_name: "Worker1", reason: "bd-123: Auth", acquired_at: "...", ttl_seconds: 3600 }]
1037
+
1038
+ // Get recent messages
1039
+ const messages = await getRecentMessages(db, { limit: 10, importance: "high" });
1040
+ // Returns: [{ id: 1, from: "Worker1", to: ["Coordinator"], subject: "BLOCKED", timestamp: "...", importance: "high" }]
1041
+
1042
+ // List all epics
1043
+ const epics = await getEpicList(db);
1044
+ // Returns: [{ epic_id: "mjmas3zxlmg", title: "Feature X", subtask_count: 5, completed_count: 3 }]
263
1045
  ```
264
1046
 
1047
+ **Use cases:**
1048
+ - Build custom TUI dashboards (see `swarm dashboard` implementation)
1049
+ - Export to monitoring systems (Prometheus, Datadog)
1050
+ - Real-time web dashboards (WebSocket + React)
1051
+ - Alerting (detect stuck tasks, file conflicts)
1052
+
265
1053
  ## Projections
266
1054
 
267
- Materialized views automatically updated from events:
1055
+ Materialized views automatically updated from events via triggers:
268
1056
 
269
- | Projection | Description |
270
- | ------------------- | ---------------------------------- |
271
- | `agents` | Active agents per project |
272
- | `messages` | Agent inbox/outbox with recipients |
273
- | `file_reservations` | Current file locks with TTL |
274
- | `swarm_contexts` | Checkpoint state for recovery |
275
- | `eval_records` | Outcome data for learning |
1057
+ | Projection | Description | Updated By |
1058
+ | ------------------- | ---------------------------------- | ---------- |
1059
+ | `agents` | Active agents per project | `agent_registered`, `agent_active` |
1060
+ | `messages` | Agent inbox/outbox with recipients | `message_sent` |
1061
+ | `message_recipients`| Many-to-many message targets | `message_sent` |
1062
+ | `message_reads` | Read receipts | `message_read` |
1063
+ | `reservations` | Current file locks with TTL | `file_reserved`, `file_released` |
1064
+ | `swarm_contexts` | Checkpoint state for recovery | `swarm_checkpointed` |
1065
+ | `eval_records` | Outcome data for learning | `eval_captured`, `eval_scored`, `eval_finalized` |
276
1066
 
277
1067
  ## Testing
278
1068
 
@@ -372,6 +1162,119 @@ interface SwarmMailAdapter {
372
1162
  }
373
1163
  ```
374
1164
 
1165
+ ## Session Indexing
1166
+
1167
+ Cross-agent session search and indexing layer for multi-agent conversation history.
1168
+
1169
+ **Inspired by [CASS (coding_agent_session_search)](https://github.com/Dicklesworthstone/coding_agent_session_search) by Dicklesworthstone** - we've adapted the session indexing concepts for TypeScript + libSQL.
1170
+
1171
+ ```
1172
+ ┌─────────────────────────────────────────────────────────────┐
1173
+ │ SESSION INDEXING ARCHITECTURE │
1174
+ ├─────────────────────────────────────────────────────────────┤
1175
+ │ │
1176
+ │ ┌──────────────┐ │
1177
+ │ │ Agent Logs │ (OpenCode, Cursor, Claude, etc.) │
1178
+ │ │ ~/.config/ │ │
1179
+ │ │ *.jsonl │ │
1180
+ │ └──────┬───────┘ │
1181
+ │ │ │
1182
+ │ ▼ │
1183
+ │ ┌──────────────────────────────────────────┐ │
1184
+ │ │ SessionParser (JSONL → Normalized) │ │
1185
+ │ │ ├─ Line-by-line parsing │ │
1186
+ │ │ └─ Multi-agent format detection │ │
1187
+ │ └──────────────┬───────────────────────────┘ │
1188
+ │ │ │
1189
+ │ ▼ │
1190
+ │ ┌──────────────────────────────────────────┐ │
1191
+ │ │ ChunkProcessor (Message-level chunks) │ │
1192
+ │ │ ├─ Sliding window (512 token chunks) │ │
1193
+ │ │ ├─ Ollama embeddings (mxbai-embed-large)│ │
1194
+ │ │ └─ Store in semantic-memory │ │
1195
+ │ └──────────────┬───────────────────────────┘ │
1196
+ │ │ │
1197
+ │ ▼ │
1198
+ │ ┌──────────────────────────────────────────┐ │
1199
+ │ │ libSQL Storage (via semantic-memory) │ │
1200
+ │ │ ├─ memories table (vector search) │ │
1201
+ │ │ ├─ metadata (agent, session, timestamp) │ │
1202
+ │ │ └─ libSQL vec extension similarity │ │
1203
+ │ └──────────────┬───────────────────────────┘ │
1204
+ │ │ │
1205
+ │ ▼ │
1206
+ │ ┌──────────────────────────────────────────┐ │
1207
+ │ │ StalenessDetector (index freshness) │ │
1208
+ │ │ ├─ Track indexed sessions + mtimes │ │
1209
+ │ │ ├─ Detect new/modified files │ │
1210
+ │ │ └─ Trigger re-indexing when needed │ │
1211
+ │ └──────────────┬───────────────────────────┘ │
1212
+ │ │ │
1213
+ │ ▼ │
1214
+ │ ┌──────────────────────────────────────────┐ │
1215
+ │ │ Search API (cass_* plugin tools) │ │
1216
+ │ │ ├─ Semantic search (vector similarity) │ │
1217
+ │ │ ├─ Full-text search (FTS5) │ │
1218
+ │ │ ├─ Pagination + field projection │ │
1219
+ │ │ └─ Session viewer (expand context) │ │
1220
+ │ └──────────────────────────────────────────┘ │
1221
+ │ │
1222
+ └─────────────────────────────────────────────────────────────┘
1223
+ ```
1224
+
1225
+ ### Components
1226
+
1227
+ | Component | Purpose | Key Methods |
1228
+ |-----------|---------|-------------|
1229
+ | **ChunkProcessor** | Message-level chunking + embedding | `processMessage()`, `generateEmbedding()` |
1230
+ | **SessionParser** | JSONL → NormalizedMessage | `parseLine()`, `detectAgent()` |
1231
+ | **SessionViewer** | Line-by-line JSONL reader | `readLines()`, `expandContext()` |
1232
+ | **StalenessDetector** | Index freshness tracking | `recordIndexed()`, `checkStaleness()` |
1233
+ | **Pagination** | Field projection for compact output | `projectSearchResult()`, `FIELD_SETS` |
1234
+
1235
+ ### Usage
1236
+
1237
+ ```typescript
1238
+ import { ChunkProcessor, StalenessDetector } from "swarm-mail";
1239
+ import { createSemanticMemory } from "swarm-mail";
1240
+
1241
+ // 1. Initialize semantic memory (vector store)
1242
+ const memory = await createSemanticMemory("/my/project");
1243
+
1244
+ // 2. Create chunk processor
1245
+ const processor = new ChunkProcessor(memory);
1246
+
1247
+ // 3. Process a session message
1248
+ const chunks = await processor.processMessage({
1249
+ session_id: "abc123",
1250
+ agent: "opencode",
1251
+ timestamp: Date.now(),
1252
+ role: "assistant",
1253
+ content: "Message text...",
1254
+ });
1255
+
1256
+ // 4. Search across indexed sessions
1257
+ const results = await memory.find("authentication error", { limit: 5 });
1258
+
1259
+ // 5. Check if re-indexing needed
1260
+ const detector = new StalenessDetector(memory);
1261
+ await detector.recordIndexed("/path/to/session.jsonl", Date.now());
1262
+ const isStale = await detector.checkStaleness("/path/to/session.jsonl");
1263
+ ```
1264
+
1265
+ ### Plugin Integration
1266
+
1267
+ The session indexing layer powers the `cass_*` plugin tools (see AGENTS.md):
1268
+
1269
+ - `cass_search` - Search across all agent histories
1270
+ - `cass_view` - View specific session
1271
+ - `cass_expand` - Expand context around line
1272
+ - `cass_index` - Build/rebuild index
1273
+ - `cass_health` - Check index readiness
1274
+ - `cass_stats` - Show index statistics
1275
+
1276
+ **Before solving problems from scratch, query the index** - another agent may have already solved it.
1277
+
375
1278
  ## Resources
376
1279
 
377
1280
  - **Documentation:** [swarmtools.ai/docs](https://swarmtools.ai/docs)