opencode-swarm-plugin 0.40.0 → 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 (59) 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 +65 -16
  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 +155 -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 +661 -732
  14. package/bin/swarm.ts +274 -0
  15. package/dist/compaction-hook.d.ts +7 -5
  16. package/dist/compaction-hook.d.ts.map +1 -1
  17. package/dist/compaction-prompt-scoring.d.ts +1 -0
  18. package/dist/compaction-prompt-scoring.d.ts.map +1 -1
  19. package/dist/eval-runner.d.ts +134 -0
  20. package/dist/eval-runner.d.ts.map +1 -0
  21. package/dist/hive.d.ts.map +1 -1
  22. package/dist/index.d.ts +29 -0
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +99741 -58858
  25. package/dist/memory-tools.d.ts +70 -2
  26. package/dist/memory-tools.d.ts.map +1 -1
  27. package/dist/memory.d.ts +37 -0
  28. package/dist/memory.d.ts.map +1 -1
  29. package/dist/observability-tools.d.ts +64 -0
  30. package/dist/observability-tools.d.ts.map +1 -1
  31. package/dist/plugin.js +99356 -58318
  32. package/dist/swarm-orchestrate.d.ts.map +1 -1
  33. package/dist/swarm-prompts.d.ts +32 -1
  34. package/dist/swarm-prompts.d.ts.map +1 -1
  35. package/docs/planning/ADR-009-oh-my-opencode-patterns.md +353 -0
  36. package/evals/ARCHITECTURE.md +1189 -0
  37. package/evals/example.eval.ts +3 -4
  38. package/evals/fixtures/compaction-prompt-cases.ts +6 -0
  39. package/evals/scorers/coordinator-discipline.ts +0 -253
  40. package/evals/swarm-decomposition.eval.ts +4 -2
  41. package/package.json +4 -3
  42. package/src/compaction-prompt-scorers.test.ts +10 -9
  43. package/src/compaction-prompt-scoring.ts +7 -5
  44. package/src/eval-runner.test.ts +128 -1
  45. package/src/eval-runner.ts +46 -0
  46. package/src/hive.ts +43 -42
  47. package/src/memory-tools.test.ts +84 -0
  48. package/src/memory-tools.ts +68 -3
  49. package/src/memory.test.ts +2 -112
  50. package/src/memory.ts +88 -49
  51. package/src/observability-tools.test.ts +13 -0
  52. package/src/observability-tools.ts +277 -0
  53. package/src/swarm-orchestrate.test.ts +162 -0
  54. package/src/swarm-orchestrate.ts +7 -5
  55. package/src/swarm-prompts.test.ts +168 -4
  56. package/src/swarm-prompts.ts +228 -7
  57. package/.env +0 -2
  58. package/.turbo/turbo-test.log +0 -481
  59. package/.turbo/turbo-typecheck.log +0 -1
package/src/hive.ts CHANGED
@@ -741,42 +741,44 @@ 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,
748
- 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,
759
- });
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
- }
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
+ }
768
770
 
769
- // Capture decomposition_complete event for eval scoring
770
- try {
771
- const { captureCoordinatorEvent } = await import("./eval-capture.js");
772
-
773
- // Build files_per_subtask map (indexed by subtask index)
774
- const filesPerSubtask: Record<number, string[]> = {};
775
- validated.subtasks.forEach((subtask, index) => {
776
- if (subtask.files && subtask.files.length > 0) {
777
- filesPerSubtask[index] = subtask.files;
778
- }
779
- });
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
+ });
780
782
 
781
783
  captureCoordinatorEvent({
782
784
  session_id: ctx.sessionID || "unknown",
@@ -792,13 +794,12 @@ export const hive_create_epic = tool({
792
794
  task: args.task,
793
795
  },
794
796
  });
795
- } catch (error) {
796
- // Non-fatal - log and continue
797
- console.warn(
798
- "[hive_create_epic] Failed to capture decomposition_complete event:",
799
- error,
800
- );
801
- }
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
+ );
802
803
  }
803
804
 
804
805
  // Sync cells to JSONL so spawned workers can see them immediately
@@ -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
  });
@@ -120,116 +120,6 @@ describe("memory adapter", () => {
120
120
  }
121
121
  });
122
122
  });
123
-
124
- describe("upsert", () => {
125
- test("returns ADD operation for new memory", async () => {
126
- const result = await adapter.upsert({
127
- information: "Completely new information about quantum computing",
128
- tags: "quantum,physics",
129
- });
130
-
131
- expect(result.operation).toBe("ADD");
132
- expect(result.reason).toBeDefined();
133
- expect(result.memoryId).toBeDefined();
134
- expect(result.memoryId).toMatch(/^mem_/);
135
- });
136
-
137
- test("returns UPDATE operation when refining existing memory", async () => {
138
- // Store initial memory
139
- await adapter.store({
140
- information: "OAuth tokens need buffer",
141
- tags: "auth",
142
- });
143
-
144
- // Try to upsert refined version
145
- const result = await adapter.upsert({
146
- information: "OAuth refresh tokens need 5min buffer before expiry to avoid race conditions",
147
- tags: "auth,oauth,tokens",
148
- });
149
-
150
- expect(result.operation).toBe("UPDATE");
151
- expect(result.reason).toBeDefined();
152
- expect(result.memoryId).toBeDefined();
153
- });
154
-
155
- test("returns NOOP operation when information already exists", async () => {
156
- // Store a memory
157
- await adapter.store({
158
- information: "Next.js 16 requires Suspense for Cache Components",
159
- tags: "nextjs",
160
- });
161
-
162
- // Try to upsert same information
163
- const result = await adapter.upsert({
164
- information: "Next.js 16 requires Suspense for Cache Components",
165
- tags: "nextjs",
166
- });
167
-
168
- expect(result.operation).toBe("NOOP");
169
- expect(result.reason).toContain("already captured");
170
- });
171
-
172
- test("autoTag generates tags when enabled", async () => {
173
- const result = await adapter.upsert({
174
- information: "TypeScript interfaces are better than type aliases for object types",
175
- autoTag: true,
176
- });
177
-
178
- expect(result.autoTags).toBeDefined();
179
- expect(result.autoTags?.tags).toBeInstanceOf(Array);
180
- expect(result.autoTags?.tags.length).toBeGreaterThan(0);
181
- });
182
-
183
- test("autoLink creates links when enabled", async () => {
184
- // Store a base memory
185
- await adapter.store({
186
- information: "TypeScript supports structural typing",
187
- tags: "typescript",
188
- });
189
-
190
- // Upsert related memory with autoLink
191
- const result = await adapter.upsert({
192
- information: "TypeScript interfaces use structural typing for shape matching",
193
- autoLink: true,
194
- });
195
-
196
- if (result.linksCreated !== undefined) {
197
- expect(result.linksCreated).toBeGreaterThanOrEqual(0);
198
- }
199
- });
200
-
201
- test("extractEntities extracts entities when enabled", async () => {
202
- const result = await adapter.upsert({
203
- information: "React 19 introduces Server Components for Next.js 15",
204
- extractEntities: true,
205
- });
206
-
207
- if (result.entitiesExtracted !== undefined) {
208
- expect(result.entitiesExtracted).toBeGreaterThanOrEqual(0);
209
- }
210
- });
211
-
212
- test("respects collection parameter", async () => {
213
- const result = await adapter.upsert({
214
- information: "Test memory in custom collection",
215
- collection: "test-collection",
216
- });
217
-
218
- expect(result.memoryId).toBeDefined();
219
- // Verify memory was stored in correct collection
220
- const memory = await adapter.get({ id: result.memoryId! });
221
- expect(memory?.collection).toBe("test-collection");
222
- });
223
-
224
- test("handles errors gracefully when information is missing", async () => {
225
- await expect(async () => {
226
- await (adapter.upsert as any)({
227
- // Missing required information field
228
- tags: "test",
229
- });
230
- }).toThrow();
231
- });
232
- });
233
123
  });
234
124
 
235
125
  describe("auto-migration on createMemoryAdapter", () => {
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 */
@@ -270,10 +278,24 @@ export async function createMemoryAdapter(
270
278
  await maybeAutoMigrate(db);
271
279
  }
272
280
 
273
- // Convert DatabaseAdapter to SwarmDb (Drizzle client) for createMemoryStore
281
+ // Convert DatabaseAdapter to SwarmDb (Drizzle client) for real swarm-mail adapter
274
282
  const drizzleDb = toSwarmDb(db);
275
- const store = createMemoryStore(drizzleDb);
276
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);
277
299
  const ollamaLayer = makeOllamaLive(config);
278
300
 
279
301
  /**
@@ -306,59 +328,39 @@ export async function createMemoryAdapter(
306
328
 
307
329
  return {
308
330
  /**
309
- * 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
310
337
  */
311
338
  async store(args: StoreArgs): Promise<StoreResult> {
312
- const id = generateId();
313
- const tags = parseTags(args.tags);
314
- const collection = args.collection ?? "default";
315
-
316
- // Parse metadata if provided
317
- let metadata: Record<string, unknown> = {};
318
- if (args.metadata) {
319
- try {
320
- metadata = JSON.parse(args.metadata);
321
- } catch {
322
- metadata = { raw: args.metadata };
323
- }
324
- }
325
-
326
- // Add tags to metadata
327
- if (tags.length > 0) {
328
- metadata.tags = tags;
329
- }
330
-
331
- // Clamp confidence to valid range [0.0, 1.0]
332
- const clampConfidence = (c: number | undefined): number => {
333
- if (c === undefined) return 0.7;
334
- return Math.max(0.0, Math.min(1.0, c));
335
- };
336
-
337
- const memory: Memory = {
338
- id,
339
- content: args.information,
340
- metadata,
341
- collection,
342
- createdAt: new Date(),
343
- confidence: clampConfidence(args.confidence),
344
- };
345
-
346
- // Generate embedding
347
- const program = Effect.gen(function* () {
348
- const ollama = yield* Ollama;
349
- 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,
350
348
  });
351
349
 
352
- const embedding = await Effect.runPromise(
353
- program.pipe(Effect.provide(ollamaLayer)),
354
- );
355
-
356
- // Store memory
357
- 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
+ }
358
360
 
359
361
  return {
360
- id,
361
- message: `Stored memory ${id} in collection: ${collection}`,
362
+ id: result.id,
363
+ message,
362
364
  };
363
365
  },
364
366
 
@@ -484,5 +486,42 @@ export async function createMemoryAdapter(
484
486
  };
485
487
  }
486
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
+ },
487
526
  };
488
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
  });