opencode-swarm-plugin 0.39.1 → 0.42.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/.hive/analysis/eval-failure-analysis-2025-12-25.md +331 -0
  2. package/.hive/analysis/session-data-quality-audit.md +320 -0
  3. package/.hive/eval-results.json +481 -24
  4. package/.hive/issues.jsonl +76 -11
  5. package/.hive/memories.jsonl +159 -1
  6. package/.opencode/eval-history.jsonl +315 -0
  7. package/.turbo/turbo-build.log +5 -5
  8. package/CHANGELOG.md +207 -0
  9. package/README.md +2 -0
  10. package/SCORER-ANALYSIS.md +598 -0
  11. package/bin/eval-gate.test.ts +158 -0
  12. package/bin/eval-gate.ts +74 -0
  13. package/bin/swarm.test.ts +1054 -719
  14. package/bin/swarm.ts +577 -0
  15. package/dist/compaction-hook.d.ts +10 -1
  16. package/dist/compaction-hook.d.ts.map +1 -1
  17. package/dist/compaction-observability.d.ts +173 -0
  18. package/dist/compaction-observability.d.ts.map +1 -0
  19. package/dist/compaction-prompt-scoring.d.ts +1 -0
  20. package/dist/compaction-prompt-scoring.d.ts.map +1 -1
  21. package/dist/eval-capture.d.ts +93 -0
  22. package/dist/eval-capture.d.ts.map +1 -1
  23. package/dist/eval-runner.d.ts +134 -0
  24. package/dist/eval-runner.d.ts.map +1 -0
  25. package/dist/hive.d.ts.map +1 -1
  26. package/dist/index.d.ts +65 -1
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +84043 -28070
  29. package/dist/memory-tools.d.ts +70 -2
  30. package/dist/memory-tools.d.ts.map +1 -1
  31. package/dist/memory.d.ts +37 -0
  32. package/dist/memory.d.ts.map +1 -1
  33. package/dist/observability-tools.d.ts +64 -0
  34. package/dist/observability-tools.d.ts.map +1 -1
  35. package/dist/plugin.js +83570 -27466
  36. package/dist/schemas/task.d.ts +3 -3
  37. package/dist/swarm-orchestrate.d.ts.map +1 -1
  38. package/dist/swarm-prompts.d.ts +32 -1
  39. package/dist/swarm-prompts.d.ts.map +1 -1
  40. package/docs/planning/ADR-009-oh-my-opencode-patterns.md +353 -0
  41. package/evals/ARCHITECTURE.md +1189 -0
  42. package/evals/README.md +113 -0
  43. package/evals/example.eval.ts +3 -4
  44. package/evals/fixtures/compaction-prompt-cases.ts +6 -0
  45. package/evals/scorers/coordinator-discipline.evalite-test.ts +163 -0
  46. package/evals/scorers/coordinator-discipline.ts +82 -2
  47. package/evals/scorers/index.test.ts +146 -0
  48. package/evals/scorers/index.ts +104 -0
  49. package/evals/swarm-decomposition.eval.ts +13 -4
  50. package/examples/commands/swarm.md +291 -21
  51. package/package.json +4 -3
  52. package/src/compaction-hook.ts +258 -110
  53. package/src/compaction-observability.integration.test.ts +139 -0
  54. package/src/compaction-observability.test.ts +187 -0
  55. package/src/compaction-observability.ts +324 -0
  56. package/src/compaction-prompt-scorers.test.ts +10 -9
  57. package/src/compaction-prompt-scoring.ts +7 -5
  58. package/src/eval-capture.test.ts +204 -1
  59. package/src/eval-capture.ts +194 -2
  60. package/src/eval-runner.test.ts +223 -0
  61. package/src/eval-runner.ts +402 -0
  62. package/src/hive.ts +57 -22
  63. package/src/index.ts +54 -1
  64. package/src/memory-tools.test.ts +84 -0
  65. package/src/memory-tools.ts +68 -3
  66. package/src/memory.test.ts +2 -2
  67. package/src/memory.ts +122 -49
  68. package/src/observability-tools.test.ts +13 -0
  69. package/src/observability-tools.ts +277 -0
  70. package/src/swarm-orchestrate.test.ts +162 -0
  71. package/src/swarm-orchestrate.ts +7 -5
  72. package/src/swarm-prompts.test.ts +168 -4
  73. package/src/swarm-prompts.ts +228 -7
  74. package/.env +0 -2
  75. package/.turbo/turbo-test.log +0 -481
  76. package/.turbo/turbo-typecheck.log +0 -1
  77. package/dist/beads.d.ts +0 -386
  78. package/dist/beads.d.ts.map +0 -1
  79. package/dist/schemas/bead-events.d.ts +0 -698
  80. package/dist/schemas/bead-events.d.ts.map +0 -1
  81. package/dist/schemas/bead.d.ts +0 -255
  82. package/dist/schemas/bead.d.ts.map +0 -1
package/src/hive.ts CHANGED
@@ -741,30 +741,65 @@ export const hive_create_epic = tool({
741
741
  };
742
742
 
743
743
  // Emit DecompositionGeneratedEvent for learning system
744
- if (args.project_key) {
745
- try {
746
- const event = createEvent("decomposition_generated", {
747
- project_key: args.project_key,
744
+ // Always emit using projectKey (from getHiveWorkingDirectory), not args.project_key
745
+ // This fixes the bug where events weren't emitted when callers didn't pass project_key
746
+ const effectiveProjectKey = args.project_key || projectKey;
747
+ try {
748
+ const event = createEvent("decomposition_generated", {
749
+ project_key: effectiveProjectKey,
750
+ epic_id: epic.id,
751
+ task: args.task || validated.epic_title,
752
+ context: validated.epic_description,
753
+ strategy: args.strategy || "feature-based",
754
+ epic_title: validated.epic_title,
755
+ subtasks: validated.subtasks.map((st) => ({
756
+ title: st.title,
757
+ files: st.files || [],
758
+ priority: st.priority,
759
+ })),
760
+ recovery_context: args.recovery_context,
761
+ });
762
+ await appendEvent(event, effectiveProjectKey);
763
+ } catch (error) {
764
+ // Non-fatal - log and continue
765
+ console.warn(
766
+ "[hive_create_epic] Failed to emit DecompositionGeneratedEvent:",
767
+ error,
768
+ );
769
+ }
770
+
771
+ // Capture decomposition_complete event for eval scoring
772
+ try {
773
+ const { captureCoordinatorEvent } = await import("./eval-capture.js");
774
+
775
+ // Build files_per_subtask map (indexed by subtask index)
776
+ const filesPerSubtask: Record<number, string[]> = {};
777
+ validated.subtasks.forEach((subtask, index) => {
778
+ if (subtask.files && subtask.files.length > 0) {
779
+ filesPerSubtask[index] = subtask.files;
780
+ }
781
+ });
782
+
783
+ captureCoordinatorEvent({
784
+ session_id: ctx.sessionID || "unknown",
748
785
  epic_id: epic.id,
749
- task: args.task || validated.epic_title,
750
- context: validated.epic_description,
751
- strategy: args.strategy || "feature-based",
752
- epic_title: validated.epic_title,
753
- subtasks: validated.subtasks.map((st) => ({
754
- title: st.title,
755
- files: st.files || [],
756
- priority: st.priority,
757
- })),
758
- recovery_context: args.recovery_context,
786
+ timestamp: new Date().toISOString(),
787
+ event_type: "DECISION",
788
+ decision_type: "decomposition_complete",
789
+ payload: {
790
+ subtask_count: validated.subtasks.length,
791
+ strategy_used: args.strategy || "feature-based",
792
+ files_per_subtask: filesPerSubtask,
793
+ epic_title: validated.epic_title,
794
+ task: args.task,
795
+ },
759
796
  });
760
- await appendEvent(event, args.project_key);
761
- } catch (error) {
762
- // Non-fatal - log and continue
763
- console.warn(
764
- "[hive_create_epic] Failed to emit DecompositionGeneratedEvent:",
765
- error,
766
- );
767
- }
797
+ } catch (error) {
798
+ // Non-fatal - log and continue
799
+ console.warn(
800
+ "[hive_create_epic] Failed to capture decomposition_complete event:",
801
+ error,
802
+ );
768
803
  }
769
804
 
770
805
  // Sync cells to JSONL so spawned workers can see them immediately
package/src/index.ts CHANGED
@@ -49,6 +49,7 @@ import { mandateTools } from "./mandates";
49
49
  import { memoryTools } from "./memory-tools";
50
50
  import { observabilityTools } from "./observability-tools";
51
51
  import { researchTools } from "./swarm-research";
52
+ import { evalTools } from "./eval-runner";
52
53
  import {
53
54
  guardrailOutput,
54
55
  DEFAULT_GUARDRAIL_CONFIG,
@@ -175,6 +176,7 @@ const SwarmPlugin: Plugin = async (
175
176
  ...memoryTools,
176
177
  ...observabilityTools,
177
178
  ...researchTools,
179
+ ...evalTools,
178
180
  },
179
181
 
180
182
  /**
@@ -729,6 +731,8 @@ export {
729
731
  * Includes:
730
732
  * - SWARM_COMPACTION_CONTEXT - Prompt text for swarm state preservation
731
733
  * - createCompactionHook - Factory function for the compaction hook
734
+ * - scanSessionMessages - Scan session for swarm state
735
+ * - ScannedSwarmState - Scanned state interface
732
736
  *
733
737
  * Usage:
734
738
  * ```typescript
@@ -739,7 +743,56 @@ export {
739
743
  * };
740
744
  * ```
741
745
  */
742
- export { SWARM_COMPACTION_CONTEXT, createCompactionHook } from "./compaction-hook";
746
+ export {
747
+ SWARM_COMPACTION_CONTEXT,
748
+ createCompactionHook,
749
+ scanSessionMessages,
750
+ type ScannedSwarmState,
751
+ } from "./compaction-hook";
752
+
753
+ /**
754
+ * Re-export compaction-observability module
755
+ *
756
+ * Includes:
757
+ * - CompactionPhase - Enum of compaction phases
758
+ * - createMetricsCollector - Create a metrics collector
759
+ * - recordPhaseStart, recordPhaseComplete - Phase timing
760
+ * - recordPatternExtracted, recordPatternSkipped - Pattern tracking
761
+ * - getMetricsSummary - Get metrics summary
762
+ *
763
+ * Types:
764
+ * - CompactionMetrics - Mutable metrics collector
765
+ * - CompactionMetricsSummary - Read-only summary snapshot
766
+ *
767
+ * Features:
768
+ * - Phase timing breakdown (START, GATHER, DETECT, INJECT, COMPLETE)
769
+ * - Pattern extraction tracking with reasons
770
+ * - Success rate calculation
771
+ * - Debug mode for verbose details
772
+ * - JSON serializable for persistence
773
+ *
774
+ * Usage:
775
+ * ```typescript
776
+ * import { createMetricsCollector, CompactionPhase, recordPhaseStart } from "opencode-swarm-plugin";
777
+ *
778
+ * const metrics = createMetricsCollector({ session_id: "abc123" });
779
+ * recordPhaseStart(metrics, CompactionPhase.DETECT);
780
+ * // ... work ...
781
+ * recordPhaseComplete(metrics, CompactionPhase.DETECT);
782
+ * const summary = getMetricsSummary(metrics);
783
+ * ```
784
+ */
785
+ export {
786
+ CompactionPhase,
787
+ createMetricsCollector,
788
+ recordPhaseStart,
789
+ recordPhaseComplete,
790
+ recordPatternExtracted,
791
+ recordPatternSkipped,
792
+ getMetricsSummary,
793
+ type CompactionMetrics,
794
+ type CompactionMetricsSummary,
795
+ } from "./compaction-observability";
743
796
 
744
797
  /**
745
798
  * Re-export memory module
@@ -24,6 +24,7 @@ describe("memory tools integration", () => {
24
24
  expect(toolNames).toContain("semantic-memory_list");
25
25
  expect(toolNames).toContain("semantic-memory_stats");
26
26
  expect(toolNames).toContain("semantic-memory_check");
27
+ expect(toolNames).toContain("semantic-memory_upsert");
27
28
  });
28
29
 
29
30
  test("tools have execute functions", () => {
@@ -108,4 +109,87 @@ describe("memory tools integration", () => {
108
109
  expect(typeof parsed.ollama).toBe("boolean");
109
110
  });
110
111
  });
112
+
113
+ describe("semantic-memory_upsert", () => {
114
+ test("returns valid ADD operation result", async () => {
115
+ const tool = memoryTools["semantic-memory_upsert"];
116
+ const result = await tool.execute(
117
+ {
118
+ information: "Test memory for plugin tool",
119
+ tags: "test,plugin",
120
+ },
121
+ { sessionID: "test-session" } as any,
122
+ );
123
+
124
+ const parsed = JSON.parse(result);
125
+
126
+ expect(parsed.operation).toBe("ADD");
127
+ expect(parsed.reason).toBeDefined();
128
+ expect(parsed.memoryId).toBeDefined();
129
+ expect(parsed.memoryId).toMatch(/^mem_/);
130
+ });
131
+
132
+ test("includes autoTags when enabled", async () => {
133
+ const tool = memoryTools["semantic-memory_upsert"];
134
+ const result = await tool.execute(
135
+ {
136
+ information: "TypeScript is a typed superset of JavaScript",
137
+ autoTag: true,
138
+ },
139
+ { sessionID: "test-session" } as any,
140
+ );
141
+
142
+ const parsed = JSON.parse(result);
143
+
144
+ expect(parsed.autoTags).toBeDefined();
145
+ expect(parsed.autoTags.tags).toBeInstanceOf(Array);
146
+ expect(parsed.autoTags.keywords).toBeInstanceOf(Array);
147
+ expect(parsed.autoTags.category).toBe("general");
148
+ });
149
+
150
+ test("includes linksCreated when autoLink enabled", async () => {
151
+ const tool = memoryTools["semantic-memory_upsert"];
152
+ const result = await tool.execute(
153
+ {
154
+ information: "React hooks enable functional components to use state",
155
+ autoLink: true,
156
+ },
157
+ { sessionID: "test-session" } as any,
158
+ );
159
+
160
+ const parsed = JSON.parse(result);
161
+
162
+ expect(parsed.linksCreated).toBeDefined();
163
+ expect(typeof parsed.linksCreated).toBe("number");
164
+ });
165
+
166
+ test("includes entitiesExtracted when extractEntities enabled", async () => {
167
+ const tool = memoryTools["semantic-memory_upsert"];
168
+ const result = await tool.execute(
169
+ {
170
+ information: "Next.js 15 was released by Vercel in October 2024",
171
+ extractEntities: true,
172
+ },
173
+ { sessionID: "test-session" } as any,
174
+ );
175
+
176
+ const parsed = JSON.parse(result);
177
+
178
+ expect(parsed.entitiesExtracted).toBeDefined();
179
+ expect(typeof parsed.entitiesExtracted).toBe("number");
180
+ });
181
+
182
+ test("throws error when information is missing", async () => {
183
+ const tool = memoryTools["semantic-memory_upsert"];
184
+
185
+ await expect(async () => {
186
+ await tool.execute(
187
+ {
188
+ tags: "test",
189
+ } as any,
190
+ { sessionID: "test-session" } as any,
191
+ );
192
+ }).toThrow("information is required");
193
+ });
194
+ });
111
195
  });
@@ -27,6 +27,9 @@ import {
27
27
  type StatsResult,
28
28
  type HealthResult,
29
29
  type OperationResult,
30
+ type UpsertArgs,
31
+ type UpsertResult,
32
+ type AutoTags,
30
33
  } from "./memory";
31
34
 
32
35
  // Re-export types for external use
@@ -41,7 +44,10 @@ export type {
41
44
  StatsResult,
42
45
  HealthResult,
43
46
  OperationResult,
44
- };
47
+ UpsertArgs,
48
+ UpsertResult,
49
+ AutoTags,
50
+ } from "./memory";
45
51
 
46
52
  // ============================================================================
47
53
  // Types
@@ -65,7 +71,7 @@ let cachedProjectPath: string | null = null;
65
71
  * @param projectPath - Project path (uses CWD if not provided)
66
72
  * @returns Memory adapter instance
67
73
  */
68
- async function getMemoryAdapter(
74
+ export async function getMemoryAdapter(
69
75
  projectPath?: string,
70
76
  ): Promise<MemoryAdapter> {
71
77
  const path = projectPath || process.cwd();
@@ -106,7 +112,7 @@ export { createMemoryAdapter };
106
112
  */
107
113
  export const semantic_memory_store = tool({
108
114
  description:
109
- "Store a memory with semantic embedding. Memories are searchable by semantic similarity and can be organized into collections. Confidence affects decay rate: high confidence (1.0) = 135 day half-life, low confidence (0.0) = 45 day half-life.",
115
+ "Store a memory with semantic embedding. Memories are searchable by semantic similarity and can be organized into collections. Confidence affects decay rate: high confidence (1.0) = 135 day half-life, low confidence (0.0) = 45 day half-life. Supports auto-tagging, auto-linking, and entity extraction via LLM.",
110
116
  args: {
111
117
  information: tool.schema
112
118
  .string()
@@ -127,6 +133,18 @@ export const semantic_memory_store = tool({
127
133
  .number()
128
134
  .optional()
129
135
  .describe("Confidence level (0.0-1.0) affecting decay rate. Higher = slower decay. Default 0.7"),
136
+ autoTag: tool.schema
137
+ .boolean()
138
+ .optional()
139
+ .describe("Auto-generate tags using LLM. Default false"),
140
+ autoLink: tool.schema
141
+ .boolean()
142
+ .optional()
143
+ .describe("Auto-link to related memories. Default false"),
144
+ extractEntities: tool.schema
145
+ .boolean()
146
+ .optional()
147
+ .describe("Extract entities (people, places, technologies). Default false"),
130
148
  },
131
149
  async execute(args, ctx: ToolContext) {
132
150
  const adapter = await getMemoryAdapter();
@@ -258,6 +276,52 @@ export const semantic_memory_check = tool({
258
276
  },
259
277
  });
260
278
 
279
+ /**
280
+ * Smart upsert - ADD, UPDATE, DELETE, or NOOP based on existing memories
281
+ */
282
+ export const semantic_memory_upsert = tool({
283
+ description:
284
+ "Smart memory storage that decides whether to ADD, UPDATE, DELETE, or skip (NOOP) based on existing memories. Uses LLM to detect duplicates, refinements, and contradictions. Auto-generates tags, links, and entities when enabled.",
285
+ args: {
286
+ information: tool.schema
287
+ .string()
288
+ .describe("The information to store (required)"),
289
+ collection: tool.schema
290
+ .string()
291
+ .optional()
292
+ .describe("Collection name (defaults to 'default')"),
293
+ tags: tool.schema
294
+ .string()
295
+ .optional()
296
+ .describe("Comma-separated tags (e.g., 'auth,tokens,oauth')"),
297
+ metadata: tool.schema
298
+ .string()
299
+ .optional()
300
+ .describe("JSON string with additional metadata"),
301
+ confidence: tool.schema
302
+ .number()
303
+ .optional()
304
+ .describe("Confidence level (0.0-1.0) affecting decay rate. Higher = slower decay. Default 0.7"),
305
+ autoTag: tool.schema
306
+ .boolean()
307
+ .optional()
308
+ .describe("Auto-generate tags using LLM. Default true"),
309
+ autoLink: tool.schema
310
+ .boolean()
311
+ .optional()
312
+ .describe("Auto-link to related memories. Default true"),
313
+ extractEntities: tool.schema
314
+ .boolean()
315
+ .optional()
316
+ .describe("Extract entities (people, places, technologies). Default false"),
317
+ },
318
+ async execute(args, ctx: ToolContext) {
319
+ const adapter = await getMemoryAdapter();
320
+ const result = await adapter.upsert(args);
321
+ return JSON.stringify(result, null, 2);
322
+ },
323
+ });
324
+
261
325
  // ============================================================================
262
326
  // Tool Registry
263
327
  // ============================================================================
@@ -276,4 +340,5 @@ export const memoryTools = {
276
340
  "semantic-memory_list": semantic_memory_list,
277
341
  "semantic-memory_stats": semantic_memory_stats,
278
342
  "semantic-memory_check": semantic_memory_check,
343
+ "semantic-memory_upsert": semantic_memory_upsert,
279
344
  } as const;
@@ -45,7 +45,7 @@ describe("memory adapter", () => {
45
45
  });
46
46
 
47
47
  expect(result.id).toBeDefined();
48
- expect(result.id).toMatch(/^mem_/);
48
+ expect(result.id).toMatch(/^mem-/); // Real swarm-mail adapter uses 'mem-' prefix
49
49
  expect(result.message).toContain("Stored memory");
50
50
  });
51
51
 
@@ -55,7 +55,7 @@ describe("memory adapter", () => {
55
55
  collection: "project-alpha",
56
56
  });
57
57
 
58
- expect(result.id).toMatch(/^mem_/);
58
+ expect(result.id).toMatch(/^mem-/); // Real swarm-mail adapter uses 'mem-' prefix
59
59
  expect(result.message).toContain("collection: project-alpha");
60
60
  });
61
61
  });
package/src/memory.ts CHANGED
@@ -39,6 +39,8 @@ import {
39
39
  legacyDatabaseExists,
40
40
  migrateLegacyMemories,
41
41
  toSwarmDb,
42
+ createMemoryAdapter as createSwarmMailAdapter,
43
+ type MemoryConfig,
42
44
  } from "swarm-mail";
43
45
 
44
46
  // ============================================================================
@@ -71,6 +73,12 @@ export interface StoreArgs {
71
73
  readonly metadata?: string;
72
74
  /** Confidence level (0.0-1.0) affecting decay rate. Higher = slower decay. Default 0.7 */
73
75
  readonly confidence?: number;
76
+ /** Auto-generate tags using LLM. Default false */
77
+ readonly autoTag?: boolean;
78
+ /** Auto-link to related memories. Default false */
79
+ readonly autoLink?: boolean;
80
+ /** Extract entities (people, places, technologies). Default false */
81
+ readonly extractEntities?: boolean;
74
82
  }
75
83
 
76
84
  /** Arguments for find operation */
@@ -129,6 +137,39 @@ export interface OperationResult {
129
137
  readonly message?: string;
130
138
  }
131
139
 
140
+ /** Arguments for upsert operation */
141
+ export interface UpsertArgs {
142
+ readonly information: string;
143
+ readonly collection?: string;
144
+ readonly tags?: string;
145
+ readonly metadata?: string;
146
+ readonly confidence?: number;
147
+ /** Auto-generate tags using LLM. Default true */
148
+ readonly autoTag?: boolean;
149
+ /** Auto-link to related memories. Default true */
150
+ readonly autoLink?: boolean;
151
+ /** Extract entities (people, places, technologies). Default false */
152
+ readonly extractEntities?: boolean;
153
+ }
154
+
155
+ /** Auto-generated tags result */
156
+ export interface AutoTags {
157
+ readonly tags: string[];
158
+ readonly keywords: string[];
159
+ readonly category: string;
160
+ }
161
+
162
+ /** Result from upsert operation */
163
+ export interface UpsertResult {
164
+ readonly operation: "ADD" | "UPDATE" | "DELETE" | "NOOP";
165
+ readonly reason: string;
166
+ readonly memoryId?: string;
167
+ readonly affectedMemoryIds?: string[];
168
+ readonly autoTags?: AutoTags;
169
+ readonly linksCreated?: number;
170
+ readonly entitiesExtracted?: number;
171
+ }
172
+
132
173
  // ============================================================================
133
174
  // Auto-Migration Logic
134
175
  // ============================================================================
@@ -206,6 +247,7 @@ export interface MemoryAdapter {
206
247
  readonly list: (args: ListArgs) => Promise<Memory[]>;
207
248
  readonly stats: () => Promise<StatsResult>;
208
249
  readonly checkHealth: () => Promise<HealthResult>;
250
+ readonly upsert: (args: UpsertArgs) => Promise<UpsertResult>;
209
251
  }
210
252
 
211
253
  /**
@@ -236,10 +278,24 @@ export async function createMemoryAdapter(
236
278
  await maybeAutoMigrate(db);
237
279
  }
238
280
 
239
- // Convert DatabaseAdapter to SwarmDb (Drizzle client) for createMemoryStore
281
+ // Convert DatabaseAdapter to SwarmDb (Drizzle client) for real swarm-mail adapter
240
282
  const drizzleDb = toSwarmDb(db);
241
- const store = createMemoryStore(drizzleDb);
242
283
  const config = getDefaultConfig();
284
+
285
+ // Create real swarm-mail adapter with Wave 1-3 features
286
+ const realAdapter = createSwarmMailAdapter(drizzleDb, config);
287
+
288
+ // DEBUG: Check if upsert exists
289
+ if (!realAdapter || typeof realAdapter.upsert !== 'function') {
290
+ console.warn('[memory] realAdapter.upsert is not available:', {
291
+ hasAdapter: !!realAdapter,
292
+ upsertType: typeof realAdapter?.upsert,
293
+ methods: realAdapter ? Object.keys(realAdapter) : []
294
+ });
295
+ }
296
+
297
+ // For backward compatibility, keep legacy adapter for methods not yet in real adapter
298
+ const store = createMemoryStore(drizzleDb);
243
299
  const ollamaLayer = makeOllamaLive(config);
244
300
 
245
301
  /**
@@ -272,59 +328,39 @@ export async function createMemoryAdapter(
272
328
 
273
329
  return {
274
330
  /**
275
- * Store a memory with embedding
331
+ * Store a memory with embedding and optional auto-features
332
+ *
333
+ * Delegates to real swarm-mail adapter which supports:
334
+ * - autoTag: LLM-powered tag generation
335
+ * - autoLink: Semantic linking to related memories
336
+ * - extractEntities: Entity extraction and knowledge graph building
276
337
  */
277
338
  async store(args: StoreArgs): Promise<StoreResult> {
278
- const id = generateId();
279
- const tags = parseTags(args.tags);
280
- const collection = args.collection ?? "default";
281
-
282
- // Parse metadata if provided
283
- let metadata: Record<string, unknown> = {};
284
- if (args.metadata) {
285
- try {
286
- metadata = JSON.parse(args.metadata);
287
- } catch {
288
- metadata = { raw: args.metadata };
289
- }
290
- }
291
-
292
- // Add tags to metadata
293
- if (tags.length > 0) {
294
- metadata.tags = tags;
295
- }
296
-
297
- // Clamp confidence to valid range [0.0, 1.0]
298
- const clampConfidence = (c: number | undefined): number => {
299
- if (c === undefined) return 0.7;
300
- return Math.max(0.0, Math.min(1.0, c));
301
- };
302
-
303
- const memory: Memory = {
304
- id,
305
- content: args.information,
306
- metadata,
307
- collection,
308
- createdAt: new Date(),
309
- confidence: clampConfidence(args.confidence),
310
- };
311
-
312
- // Generate embedding
313
- const program = Effect.gen(function* () {
314
- const ollama = yield* Ollama;
315
- return yield* ollama.embed(args.information);
339
+ // Delegate to real swarm-mail adapter
340
+ const result = await realAdapter.store(args.information, {
341
+ collection: args.collection,
342
+ tags: args.tags,
343
+ metadata: args.metadata,
344
+ confidence: args.confidence,
345
+ autoTag: args.autoTag,
346
+ autoLink: args.autoLink,
347
+ extractEntities: args.extractEntities,
316
348
  });
317
349
 
318
- const embedding = await Effect.runPromise(
319
- program.pipe(Effect.provide(ollamaLayer)),
320
- );
321
-
322
- // Store memory
323
- await store.store(memory, embedding);
350
+ // Build user-facing message
351
+ let message = `Stored memory ${result.id} in collection: ${args.collection ?? "default"}`;
352
+
353
+ if (result.autoTags) {
354
+ message += `\nAuto-tags: ${result.autoTags.tags.join(", ")}`;
355
+ }
356
+
357
+ if (result.links && result.links.length > 0) {
358
+ message += `\nLinked to ${result.links.length} related memor${result.links.length === 1 ? "y" : "ies"}`;
359
+ }
324
360
 
325
361
  return {
326
- id,
327
- message: `Stored memory ${id} in collection: ${collection}`,
362
+ id: result.id,
363
+ message,
328
364
  };
329
365
  },
330
366
 
@@ -450,5 +486,42 @@ export async function createMemoryAdapter(
450
486
  };
451
487
  }
452
488
  },
489
+
490
+ /**
491
+ * Smart upsert - uses LLM to decide ADD, UPDATE, DELETE, or NOOP
492
+ *
493
+ * Delegates to real swarm-mail adapter with Mem0 pattern:
494
+ * - Finds semantically similar memories
495
+ * - LLM analyzes and decides operation
496
+ * - Executes with graceful degradation on LLM failures
497
+ */
498
+ async upsert(args: UpsertArgs): Promise<UpsertResult> {
499
+ // Validate required fields
500
+ if (!args.information) {
501
+ throw new Error("information is required for upsert");
502
+ }
503
+
504
+ // Delegate to real swarm-mail adapter with useSmartOps enabled
505
+ const result = await realAdapter.upsert(args.information, {
506
+ collection: args.collection,
507
+ tags: args.tags,
508
+ metadata: args.metadata,
509
+ confidence: args.confidence,
510
+ useSmartOps: true, // Enable LLM-powered decision making
511
+ autoTag: args.autoTag,
512
+ autoLink: args.autoLink,
513
+ extractEntities: args.extractEntities,
514
+ });
515
+
516
+ // Map real adapter result to plugin UpsertResult format
517
+ return {
518
+ operation: result.operation,
519
+ reason: result.reason,
520
+ memoryId: result.id,
521
+ affectedMemoryIds: [result.id],
522
+ // Note: Real adapter doesn't return autoTags/links from upsert yet
523
+ // Those are only on store(). This is consistent with current behavior.
524
+ };
525
+ },
453
526
  };
454
527
  }
@@ -343,4 +343,17 @@ describe("observability-tools", () => {
343
343
  }
344
344
  });
345
345
  });
346
+
347
+ describe("CLI Stats Helpers", () => {
348
+ // These helpers will be exported for use in bin/swarm.ts
349
+ // They format analytics data for beautiful CLI output
350
+
351
+ describe("formatSwarmStatsBox", () => {
352
+ test("formats stats in a beautiful box", () => {
353
+ // This will be implemented in observability-tools.ts
354
+ // Just defining the test structure for now
355
+ expect(true).toBe(true);
356
+ });
357
+ });
358
+ });
346
359
  });