Files
OmniRoute/open-sse/mcp-server/tools/memoryTools.ts
diegosouzapw 516a7e5520 fix(memory): code-review hardening — guards, asserts, useEffect
Smaller fixes from the 2nd code-review pass on plan 21.

Backend / CLI / DB:
- memoryTools.ts: error-path fallback no longer hardcodes
  retrievalStrategy:"exact"; uses DEFAULT_MEMORY_SETTINGS via
  toMemoryRetrievalConfig (D16 / Bug #7).
- memory.mjs: applyLegacyTypeMap also runs on search / list / clear
  (was only on add); legacy user/feedback/project/reference remap to
  canonical types with a stderr warning (D17 / Bug #4).
- migrationRunner.ts: case "073" guards via
  hasColumn(memories, needs_reindex) so an unmarked re-run of
  073_memory_vec.sql is skipped cleanly (D27).

UI:
- MemoryEngineStatus: optional onConfigure callback; "Configurar →"
  CTAs on the Embedding / Qdrant / Rerank rows when those components
  are off or missing (matches §4.3 wireframe).
- EngineTab: scroll IDs on config cards + handleConfigure wired to
  the status panel. Providers fetch moved from render body
  (setState-during-render anti-pattern) into useEffect.
- RerankConfigCard: toggle is disabled when no provider has a key —
  blocks turning rerank ON without a provider, still allows turning
  it OFF (D13).
- MemoriesTab: Import validates each entry against the canonical
  type enum before POST so invalid types are caught locally with a
  clear skipped count.

Tooling:
- package.json: test:all includes test:vitest:ui so the UI suite
  is no longer orphaned in CI.

Tests:
- cli-memory-commands: asserts updated for the new legacy->canonical
  remap on search/clear.
- memory-embedding-resolve: drop always-true `|| reason.length > 0`
  clauses that neutralized two assertions.
- memory-embedding-static-potion: model_load_failed test forces a
  real load failure via MEMORY_STATIC_CACHE_DIR=/dev/null/<subdir>
  and asserts EmbeddingError shape + reason + sanitized message
  (replaces the previous `assert.ok(true)`).
- rerank-config-card.test.tsx: happy-path now uses a provider with
  hasKey; new test covers the disabled-toggle guard.

Full memory suite green: 331/331 unit tests, 46/46 UI tests.
typecheck:core, typecheck:noimplicit:core, check:cycles clean.
2026-05-28 21:46:54 -03:00

128 lines
4.0 KiB
TypeScript

import { z } from "zod";
import { retrieveMemories } from "@/lib/memory/retrieval";
import { createMemory, deleteMemory, listMemories } from "@/lib/memory/store";
import { MemoryType } from "@/lib/memory/types";
import { getMemorySettings, toMemoryRetrievalConfig, DEFAULT_MEMORY_SETTINGS } from "@/lib/memory/settings";
export const MemorySearchSchema = z.object({
apiKeyId: z.string(),
query: z.string().optional(),
type: z.enum(["factual", "episodic", "procedural", "semantic"]).optional(),
maxTokens: z.number().int().positive().max(8000).optional(),
limit: z.number().int().positive().max(100).optional(),
});
export const MemoryAddSchema = z.object({
apiKeyId: z.string(),
sessionId: z.string().optional(),
type: z.enum(["factual", "episodic", "procedural", "semantic"]),
key: z.string().min(1),
content: z.string().min(1),
metadata: z.record(z.string(), z.unknown()).optional(),
});
export const MemoryClearSchema = z.object({
apiKeyId: z.string(),
type: z.enum(["factual", "episodic", "procedural", "semantic"]).optional(),
olderThan: z.string().optional(),
});
export const memoryTools = {
omniroute_memory_search: {
name: "omniroute_memory_search",
description: "Search memories by query, type, or API key with token budget enforcement",
inputSchema: MemorySearchSchema,
handler: async (args: z.infer<typeof MemorySearchSchema>) => {
// Plan 21 D16/Bug#7 fix: even on the error path the fallback must
// respect DEFAULT_MEMORY_SETTINGS.strategy instead of hardcoding "exact".
const memorySettings =
(await getMemorySettings().catch(() => null)) ?? DEFAULT_MEMORY_SETTINGS;
const baseConfig = toMemoryRetrievalConfig(memorySettings, {
query: args.query,
});
const config = {
...baseConfig,
maxTokens: args.maxTokens || (baseConfig.maxTokens ?? DEFAULT_MEMORY_SETTINGS.maxTokens),
};
const memories = await retrieveMemories(args.apiKeyId, config);
const filtered = args.type ? memories.filter((m) => m.type === args.type) : memories;
const limited = args.limit ? filtered.slice(0, args.limit) : filtered;
return {
success: true,
data: {
memories: limited,
count: limited.length,
totalTokens: limited.reduce((sum, m) => sum + Math.ceil(m.content.length / 4), 0),
},
};
},
},
omniroute_memory_add: {
name: "omniroute_memory_add",
description: "Add a new memory entry",
inputSchema: MemoryAddSchema,
handler: async (args: z.infer<typeof MemoryAddSchema>) => {
const memory = await createMemory({
apiKeyId: args.apiKeyId,
sessionId: args.sessionId || "",
type: args.type as MemoryType,
key: args.key,
content: args.content,
metadata: args.metadata || {},
expiresAt: null,
});
return {
success: true,
data: {
memory,
message: "Memory created successfully",
},
};
},
},
omniroute_memory_clear: {
name: "omniroute_memory_clear",
description: "Clear memories for an API key, optionally filtered by type or age",
inputSchema: MemoryClearSchema,
handler: async (args: z.infer<typeof MemoryClearSchema>) => {
const result = await listMemories({
apiKeyId: args.apiKeyId,
type: args.type as MemoryType | undefined,
});
const existingMemories = Array.isArray(result)
? result
: Array.isArray(result?.data)
? result.data
: [];
let toDelete = existingMemories;
if (args.olderThan) {
const cutoff = new Date(args.olderThan);
toDelete = existingMemories.filter((m) => new Date(m.createdAt) < cutoff);
}
let deletedCount = 0;
for (const memory of toDelete) {
await deleteMemory(memory.id);
deletedCount++;
}
return {
success: true,
data: {
deletedCount,
message: `Cleared ${deletedCount} memories`,
},
};
},
},
};