diff --git a/AGENTS.md b/AGENTS.md
index 0330b14f..ae0d577f 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -117,7 +117,7 @@ Hook scripts in `src/hooks/` are standalone Node.js scripts (no iii-sdk import).
## Current Stats (v0.9.16)
- 53 MCP tools (8 visible by default, `AGENTMEMORY_TOOLS=all` for all)
-- 125 REST endpoints
+- 126 REST endpoints
- 6 MCP resources, 3 MCP prompts
- 12 hooks, 4 skills
- 50+ iii functions
diff --git a/README.md b/README.md
index 862e540f..d1eaa35f 100644
--- a/README.md
+++ b/README.md
@@ -1429,7 +1429,7 @@ Create `~/.agentmemory/.env`:

-125 endpoints on port `3111`. The REST API binds to `127.0.0.1` by default. Protected endpoints require `Authorization: Bearer ` when `AGENTMEMORY_SECRET` is set, and mesh sync endpoints require `AGENTMEMORY_SECRET` on both peers.
+126 endpoints on port `3111`. The REST API binds to `127.0.0.1` by default. Protected endpoints require `Authorization: Bearer ` when `AGENTMEMORY_SECRET` is set, and mesh sync endpoints require `AGENTMEMORY_SECRET` on both peers.
Key endpoints
diff --git a/src/config.ts b/src/config.ts
index 1fe70465..b4f8979d 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -375,6 +375,20 @@ export function getConsolidationDecayDays(): number {
return safeParseInt(getMergedEnv()["CONSOLIDATION_DECAY_DAYS"], 30);
}
+export function isHighOrderSearchEnabled(): boolean {
+ const env = getMergedEnv();
+ const explicit = env["AGENTMEMORY_HIGH_ORDER_SEARCH"];
+ if (explicit === "false" || explicit === "0") return false;
+ if (explicit === "true" || explicit === "1") return true;
+ return isConsolidationEnabled();
+}
+
+export function getHighOrderConfidenceFloor(): number {
+ const env = getMergedEnv();
+ const raw = parseFloat(env["AGENTMEMORY_SMART_SEARCH_CONFIDENCE_FLOOR"] || "0.3");
+ return Number.isFinite(raw) && raw >= 0 && raw <= 1 ? raw : 0.3;
+}
+
export function isStandaloneMcp(): boolean {
return getMergedEnv()["STANDALONE_MCP"] === "true";
}
diff --git a/src/functions/consolidation-pipeline.ts b/src/functions/consolidation-pipeline.ts
index e51ab97c..39750101 100644
--- a/src/functions/consolidation-pipeline.ts
+++ b/src/functions/consolidation-pipeline.ts
@@ -17,7 +17,8 @@ import {
import { recordAudit } from "./audit.js";
import { getConsolidationDecayDays, isConsolidationEnabled } from "../config.js";
import { logger } from "../logger.js";
-
+import { getEmbeddingProvider } from "./search.js";
+import { float32ToBase64 } from "../state/vector-index.js";
function applyDecay(
items: Array<{
strength: number;
@@ -93,6 +94,8 @@ export function registerConsolidationPipelineFunction(
const confidence = Number.isNaN(parsedConf) ? 0.5 : parsedConf;
const fact = match[2].trim();
+ const ep = getEmbeddingProvider();
+
const existing = existingSemantic.find(
(s) => s.fact.toLowerCase() === fact.toLowerCase(),
);
@@ -101,6 +104,14 @@ export function registerConsolidationPipelineFunction(
existing.lastAccessedAt = now;
existing.updatedAt = now;
existing.confidence = Math.max(existing.confidence, confidence);
+ if (!existing.embedding && ep) {
+ try {
+ existing.embedding = float32ToBase64(await ep.embed(existing.fact));
+ existing.embeddingModel = ep.name;
+ } catch (e) {
+ logger.warn("Failed to embed existing semantic fact", { id: existing.id, error: String(e) });
+ }
+ }
await kv.set(KV.semantic, existing.id, existing);
} else {
const sem: SemanticMemory = {
@@ -115,6 +126,14 @@ export function registerConsolidationPipelineFunction(
createdAt: now,
updatedAt: now,
};
+ if (ep) {
+ try {
+ sem.embedding = float32ToBase64(await ep.embed(sem.fact));
+ sem.embeddingModel = ep.name;
+ } catch (e) {
+ logger.warn("Failed to embed new semantic fact", { error: String(e) });
+ }
+ }
await kv.set(KV.semantic, sem.id, sem);
newFacts++;
}
@@ -187,6 +206,8 @@ export function registerConsolidationPipelineFunction(
steps.push(stepMatch[1].trim());
}
+ const ep = getEmbeddingProvider();
+
const existing = existingProcs.find(
(p) => p.name.toLowerCase() === name.toLowerCase(),
);
@@ -194,6 +215,15 @@ export function registerConsolidationPipelineFunction(
existing.frequency++;
existing.updatedAt = now;
existing.strength = Math.min(1, existing.strength + 0.1);
+ if (!existing.embedding && ep) {
+ try {
+ const text = `${existing.name} ${existing.triggerCondition} ${existing.steps.join(" ")}`;
+ existing.embedding = float32ToBase64(await ep.embed(text));
+ existing.embeddingModel = ep.name;
+ } catch (e) {
+ logger.warn("Failed to embed existing procedural skill", { id: existing.id, error: String(e) });
+ }
+ }
await kv.set(KV.procedural, existing.id, existing);
} else {
const proc: ProceduralMemory = {
@@ -207,6 +237,15 @@ export function registerConsolidationPipelineFunction(
createdAt: now,
updatedAt: now,
};
+ if (ep) {
+ try {
+ const text = `${proc.name} ${proc.triggerCondition} ${proc.steps.join(" ")}`;
+ proc.embedding = float32ToBase64(await ep.embed(text));
+ proc.embeddingModel = ep.name;
+ } catch (e) {
+ logger.warn("Failed to embed new procedural skill", { error: String(e) });
+ }
+ }
await kv.set(KV.procedural, proc.id, proc);
newProcs++;
}
diff --git a/src/functions/crystallize.ts b/src/functions/crystallize.ts
index 1f0b8431..a950c9c7 100644
--- a/src/functions/crystallize.ts
+++ b/src/functions/crystallize.ts
@@ -3,6 +3,10 @@ import type { StateKV } from "../state/kv.js";
import { KV, generateId } from "../state/schema.js";
import type { Action, ActionEdge, Crystal, MemoryProvider } from "../types.js";
+import { getEmbeddingProvider } from "./search.js";
+import { float32ToBase64 } from "../state/vector-index.js";
+import { logger } from "../logger.js";
+
interface CrystalDigest {
narrative: string;
keyOutcomes: string[];
@@ -52,6 +56,7 @@ export function registerCrystallizeFunction(
);
const prompt = buildChainText(actions, relevantEdges);
+ const ep = getEmbeddingProvider();
try {
const response = await provider.summarize(CRYSTALLIZE_SYSTEM, prompt);
@@ -69,6 +74,16 @@ export function registerCrystallizeFunction(
createdAt: new Date().toISOString(),
};
+ if (ep) {
+ try {
+ const text = `${crystal.narrative} ${crystal.lessons.join(" ")}`;
+ crystal.embedding = float32ToBase64(await ep.embed(text));
+ crystal.embeddingModel = ep.name;
+ } catch (e) {
+ logger.warn("Failed to embed new crystal", { error: String(e) });
+ }
+ }
+
await kv.set(KV.crystals, crystal.id, crystal);
await Promise.all(
diff --git a/src/functions/high-order-backfill.ts b/src/functions/high-order-backfill.ts
new file mode 100644
index 00000000..2b919a85
--- /dev/null
+++ b/src/functions/high-order-backfill.ts
@@ -0,0 +1,122 @@
+import type { ISdk } from "iii-sdk";
+import type { StateKV } from "../state/kv.js";
+import { KV } from "../state/schema.js";
+import type { SemanticMemory, ProceduralMemory, Crystal, Insight } from "../types.js";
+import { getEmbeddingProvider } from "./search.js";
+import { float32ToBase64 } from "../state/vector-index.js";
+import { logger } from "../logger.js";
+
+const BACKFILL_BATCH_SIZE = 20;
+
+export function registerHighOrderBackfillFunction(sdk: ISdk, kv: StateKV): void {
+ sdk.registerFunction("mem::backfill-embeddings::high-order", async () => {
+ const ep = getEmbeddingProvider();
+ if (!ep) {
+ return { success: false, error: "No embedding provider available" };
+ }
+
+ const results = {
+ semantic: 0,
+ procedural: 0,
+ crystals: 0,
+ insights: 0,
+ };
+
+ try {
+ // 1. Semantic Facts
+ const semantics = await kv.list(KV.semantic);
+ const semToUpdate = semantics.filter(
+ (s) => !s.embedding || s.embeddingModel !== ep.name
+ );
+ for (let i = 0; i < semToUpdate.length; i += BACKFILL_BATCH_SIZE) {
+ const batch = semToUpdate.slice(i, i + BACKFILL_BATCH_SIZE);
+ const texts = batch.map((s) => s.fact);
+ try {
+ const vectors = await ep.embedBatch(texts);
+ for (let j = 0; j < batch.length; j++) {
+ batch[j].embedding = float32ToBase64(vectors[j]);
+ batch[j].embeddingModel = ep.name;
+ await kv.set(KV.semantic, batch[j].id, batch[j]);
+ }
+ results.semantic += batch.length;
+ } catch (e) {
+ logger.warn("Semantic backfill batch failed", { error: String(e) });
+ }
+ }
+
+ // 2. Procedural Skills
+ const procedurals = await kv.list(KV.procedural);
+ const procToUpdate = procedurals.filter(
+ (p) => !p.embedding || p.embeddingModel !== ep.name
+ );
+ for (let i = 0; i < procToUpdate.length; i += BACKFILL_BATCH_SIZE) {
+ const batch = procToUpdate.slice(i, i + BACKFILL_BATCH_SIZE);
+ const texts = batch.map((p) => `${p.name} ${p.triggerCondition} ${p.steps.join(" ")}`);
+ try {
+ const vectors = await ep.embedBatch(texts);
+ for (let j = 0; j < batch.length; j++) {
+ batch[j].embedding = float32ToBase64(vectors[j]);
+ batch[j].embeddingModel = ep.name;
+ await kv.set(KV.procedural, batch[j].id, batch[j]);
+ }
+ results.procedural += batch.length;
+ } catch (e) {
+ logger.warn("Procedural backfill batch failed", { error: String(e) });
+ }
+ }
+
+ // 3. Crystals
+ const crystals = await kv.list(KV.crystals);
+ const crysToUpdate = crystals.filter(
+ (c) => !c.embedding || c.embeddingModel !== ep.name
+ );
+ for (let i = 0; i < crysToUpdate.length; i += BACKFILL_BATCH_SIZE) {
+ const batch = crysToUpdate.slice(i, i + BACKFILL_BATCH_SIZE);
+ const texts = batch.map((c) => `${c.narrative} ${c.lessons.join(" ")}`);
+ try {
+ const vectors = await ep.embedBatch(texts);
+ for (let j = 0; j < batch.length; j++) {
+ batch[j].embedding = float32ToBase64(vectors[j]);
+ batch[j].embeddingModel = ep.name;
+ await kv.set(KV.crystals, batch[j].id, batch[j]);
+ }
+ results.crystals += batch.length;
+ } catch (e) {
+ logger.warn("Crystal backfill batch failed", { error: String(e) });
+ }
+ }
+
+ // 4. Insights
+ const insights = await kv.list(KV.insights);
+ const insToUpdate = insights.filter(
+ (ins) => !ins.deleted && (!ins.embedding || ins.embeddingModel !== ep.name)
+ );
+ for (let i = 0; i < insToUpdate.length; i += BACKFILL_BATCH_SIZE) {
+ const batch = insToUpdate.slice(i, i + BACKFILL_BATCH_SIZE);
+ const texts = batch.map((ins) => `${ins.title} ${ins.content}`);
+ try {
+ const vectors = await ep.embedBatch(texts);
+ for (let j = 0; j < batch.length; j++) {
+ batch[j].embedding = float32ToBase64(vectors[j]);
+ batch[j].embeddingModel = ep.name;
+ await kv.set(KV.insights, batch[j].id, batch[j]);
+ }
+ results.insights += batch.length;
+ } catch (e) {
+ logger.warn("Insight backfill batch failed", { error: String(e) });
+ }
+ }
+
+ const total = results.semantic + results.procedural + results.crystals + results.insights;
+ if (total > 0) {
+ logger.info("High-order embedding backfill complete", { backfilled: results });
+ }
+
+ return { success: true, backfilled: results };
+ } catch (err) {
+ const errorMsg = err instanceof Error ? err.message : String(err);
+ logger.error("High-order backfill encountered a fatal error", { error: errorMsg });
+ return { success: false, error: errorMsg };
+ }
+ });
+}
diff --git a/src/functions/high-order-search.ts b/src/functions/high-order-search.ts
new file mode 100644
index 00000000..ba1b9af7
--- /dev/null
+++ b/src/functions/high-order-search.ts
@@ -0,0 +1,198 @@
+import type { StateKV } from "../state/kv.js";
+import type {
+ SemanticMemory,
+ ProceduralMemory,
+ Crystal,
+ Insight,
+ HighOrderTier,
+ CompactHighOrderResult,
+} from "../types.js";
+import { KV } from "../state/schema.js";
+
+const TIER_CAP = 50;
+const CONTENT_PREVIEW_CHARS = 240;
+
+interface TierCandidate {
+ id: string;
+ tier: HighOrderTier;
+ text: string;
+ content: string;
+ confidence: number;
+ project?: string;
+ createdAt: string;
+}
+
+import { getEmbeddingProvider } from "./search.js";
+import { base64ToFloat32 } from "../state/vector-index.js";
+
+function cosineSimilarity(a: Float32Array, b: Float32Array): number {
+ if (a.length !== b.length) return 0;
+ let dot = 0, normA = 0, normB = 0;
+ for (let i = 0; i < a.length; i++) {
+ dot += a[i] * b[i];
+ normA += a[i] * a[i];
+ normB += b[i] * b[i];
+ }
+ const denom = Math.sqrt(normA) * Math.sqrt(normB);
+ return denom === 0 ? 0 : dot / denom;
+}
+
+export async function searchHighOrderTiers(
+ kv: StateKV,
+ query: string,
+ options: {
+ confidenceFloor: number;
+ project?: string;
+ limit?: number;
+ },
+): Promise<{ results: CompactHighOrderResult[], needsBackfill: boolean }> {
+ const ep = getEmbeddingProvider();
+ const [semantics, procedurals, crystals, insights] = await Promise.all([
+ kv.list(KV.semantic).catch(() => []),
+ kv.list(KV.procedural).catch(() => []),
+ kv.list(KV.crystals).catch(() => []),
+ kv.list(KV.insights).catch(() => []),
+ ]);
+
+ let needsBackfill = false;
+ let queryVector: Float32Array | null = null;
+ if (ep) {
+ try {
+ queryVector = await ep.embed(query);
+ } catch {
+ // Ignore embed error
+ }
+ }
+
+ const candidates: Array = [];
+
+ for (const s of semantics) {
+ const effectiveConfidence = Math.min(s.confidence, s.strength);
+ if (effectiveConfidence < options.confidenceFloor) continue;
+ if (ep && (!s.embedding || s.embeddingModel !== ep.name)) needsBackfill = true;
+ candidates.push({
+ id: s.id,
+ tier: "semantic",
+ text: s.fact,
+ content: truncate(s.fact),
+ confidence: effectiveConfidence,
+ createdAt: s.createdAt,
+ embedding: s.embedding ? base64ToFloat32(s.embedding) : undefined,
+ });
+ }
+
+ for (const p of procedurals) {
+ if (p.strength < options.confidenceFloor) continue;
+ if (ep && (!p.embedding || p.embeddingModel !== ep.name)) needsBackfill = true;
+ const text = `${p.name} ${p.triggerCondition} ${p.steps.join(" ")} ${(p.tags || []).join(" ")}`;
+ candidates.push({
+ id: p.id,
+ tier: "procedural",
+ text,
+ content: truncate(`${p.name}: ${p.triggerCondition}`),
+ confidence: p.strength,
+ createdAt: p.createdAt,
+ embedding: p.embedding ? base64ToFloat32(p.embedding) : undefined,
+ });
+ }
+
+ for (const c of crystals) {
+ if (options.project && c.project && c.project !== options.project) continue;
+ if (ep && (!c.embedding || c.embeddingModel !== ep.name)) needsBackfill = true;
+ const text = `${c.narrative} ${c.keyOutcomes.join(" ")} ${c.lessons.join(" ")}`;
+ candidates.push({
+ id: c.id,
+ tier: "crystal",
+ text,
+ content: truncate(c.narrative),
+ confidence: 1.0,
+ project: c.project,
+ createdAt: c.createdAt,
+ embedding: c.embedding ? base64ToFloat32(c.embedding) : undefined,
+ });
+ }
+
+ for (const i of insights) {
+ if (i.deleted) continue;
+ if (i.confidence < options.confidenceFloor) continue;
+ if (options.project && i.project && i.project !== options.project) continue;
+ if (ep && (!i.embedding || i.embeddingModel !== ep.name)) needsBackfill = true;
+ const text = `${i.title} ${i.content} ${(i.tags || []).join(" ")}`;
+ candidates.push({
+ id: i.id,
+ tier: "insight",
+ text,
+ content: truncate(`${i.title}: ${i.content}`),
+ confidence: i.confidence,
+ project: i.project,
+ createdAt: i.createdAt,
+ embedding: i.embedding ? base64ToFloat32(i.embedding) : undefined,
+ });
+ }
+
+ const terms = query.toLowerCase().split(/\s+/).filter((t) => t.length > 1);
+
+ // Scored candidates
+ const bm25Scored = candidates.map((c) => {
+ const lower = c.text.toLowerCase();
+ const matchCount = terms.length === 0 ? 0 : terms.filter((t) => lower.includes(t)).length;
+ const relevance = terms.length === 0 ? 0 : matchCount / terms.length;
+ const confBoost = c.confidence > 0.85 ? 1.2 : 1.0;
+ return { candidate: c, score: relevance * c.confidence * confBoost };
+ }).filter((x) => x.score > 0).sort((a, b) => b.score - a.score);
+
+ const vectorScored = candidates.map((c) => {
+ const score = (queryVector && c.embedding) ? cosineSimilarity(queryVector, c.embedding) : 0;
+ return { candidate: c, score: score * c.confidence };
+ }).filter((x) => x.score > 0).sort((a, b) => b.score - a.score);
+
+ // RRF
+ const rrfMap = new Map();
+ const K = 60;
+
+ bm25Scored.forEach((item, index) => {
+ const rank = index + 1;
+ rrfMap.set(item.candidate.id, { candidate: item.candidate, rrfScore: 1 / (K + rank) });
+ });
+
+ vectorScored.forEach((item, index) => {
+ const rank = index + 1;
+ const existing = rrfMap.get(item.candidate.id);
+ if (existing) {
+ existing.rrfScore += 1 / (K + rank);
+ } else {
+ rrfMap.set(item.candidate.id, { candidate: item.candidate, rrfScore: 1 / (K + rank) });
+ }
+ });
+
+ const finalScored = Array.from(rrfMap.values())
+ .sort((a, b) => b.rrfScore - a.rrfScore)
+ .map(x => {
+ const c = x.candidate;
+ return {
+ id: c.id,
+ tier: c.tier as HighOrderTier,
+ content: c.content,
+ score: Math.round(x.rrfScore * 10000) / 10000,
+ confidence: c.confidence,
+ project: c.project,
+ createdAt: c.createdAt,
+ };
+ });
+
+ const tierCounts = new Map();
+ const capped: CompactHighOrderResult[] = [];
+ for (const r of finalScored) {
+ const count = tierCounts.get(r.tier) || 0;
+ if (count >= TIER_CAP) continue;
+ tierCounts.set(r.tier, count + 1);
+ capped.push(r);
+ }
+
+ return { results: capped.slice(0, options.limit ?? 20), needsBackfill };
+}
+
+function truncate(text: string): string {
+ if (text.length <= CONTENT_PREVIEW_CHARS) return text;
+ return text.slice(0, CONTENT_PREVIEW_CHARS) + "…";
+}
diff --git a/src/functions/reflect.ts b/src/functions/reflect.ts
index 3a2af829..c71c470a 100644
--- a/src/functions/reflect.ts
+++ b/src/functions/reflect.ts
@@ -12,6 +12,9 @@ import type {
} from "../types.js";
import { recordAudit } from "./audit.js";
import { REFLECT_SYSTEM, buildReflectPrompt } from "../prompts/reflect.js";
+import { getEmbeddingProvider } from "./search.js";
+import { float32ToBase64 } from "../state/vector-index.js";
+import { logger } from "../logger.js";
interface ConceptCluster {
concepts: string[];
@@ -278,9 +281,19 @@ export function registerReflectFunctions(
const fp = fingerprintId("ins", content.trim().toLowerCase());
const existing = await kv.get(KV.insights, fp);
+ const ep = getEmbeddingProvider();
if (existing && !existing.deleted) {
reinforceInsight(existing);
+ if (!existing.embedding && ep) {
+ try {
+ const text = `${existing.title} ${existing.content}`;
+ existing.embedding = float32ToBase64(await ep.embed(text));
+ existing.embeddingModel = ep.name;
+ } catch (e) {
+ logger.warn("Failed to embed existing insight", { id: existing.id, error: String(e) });
+ }
+ }
await kv.set(KV.insights, existing.id, existing);
reinforced++;
} else {
@@ -301,6 +314,15 @@ export function registerReflectFunctions(
updatedAt: now,
decayRate: 0.05,
};
+ if (ep) {
+ try {
+ const text = `${insight.title} ${insight.content}`;
+ insight.embedding = float32ToBase64(await ep.embed(text));
+ insight.embeddingModel = ep.name;
+ } catch (e) {
+ logger.warn("Failed to embed new insight", { error: String(e) });
+ }
+ }
await kv.set(KV.insights, insight.id, insight);
newInsights++;
}
diff --git a/src/functions/smart-search.ts b/src/functions/smart-search.ts
index fbdaad0d..d08889df 100644
--- a/src/functions/smart-search.ts
+++ b/src/functions/smart-search.ts
@@ -1,15 +1,21 @@
import type { ISdk } from "iii-sdk";
import type {
+ CompactHighOrderResult,
CompactLessonResult,
CompactSearchResult,
CompressedObservation,
+ Crystal,
HybridSearchResult,
+ Insight,
Lesson,
+ ProceduralMemory,
+ SemanticMemory,
} from "../types.js";
import { KV } from "../state/schema.js";
import { StateKV } from "../state/kv.js";
import { recordAccessBatch } from "./access-tracker.js";
-import { getAgentId, isAgentScopeIsolated } from "../config.js";
+import { getAgentId, isAgentScopeIsolated, isHighOrderSearchEnabled, getHighOrderConfidenceFloor } from "../config.js";
+import { searchHighOrderTiers } from "./high-order-search.js";
import { logger } from "../logger.js";
// Compact mode trims each lesson's content for at-a-glance display. The
@@ -28,9 +34,7 @@ export function registerSmartSearchFunction(
limit?: number;
project?: string;
includeLessons?: boolean;
- // optional per-call agent filter for runtimes routing many
- // roles through one server. "*" opts out of the env-default
- // scope and returns hits from every agent.
+ includeHighOrder?: boolean;
agentId?: string;
}) => {
@@ -57,6 +61,27 @@ export function registerSmartSearchFunction(
return null;
}).filter((item): item is NonNullable => item !== null);
+ const highOrderItems: Array<{ tier: string; id: string; data: unknown }> = [];
+ const obsItems: typeof items = [];
+
+ for (const item of items) {
+ if (item.obsId.startsWith("sem_")) {
+ const entry = await kv.get(KV.semantic, item.obsId).catch(() => null);
+ if (entry) highOrderItems.push({ tier: "semantic", id: entry.id, data: entry });
+ } else if (item.obsId.startsWith("proc_") || item.obsId.startsWith("skill_")) {
+ const entry = await kv.get(KV.procedural, item.obsId).catch(() => null);
+ if (entry) highOrderItems.push({ tier: "procedural", id: entry.id, data: entry });
+ } else if (item.obsId.startsWith("crys_")) {
+ const entry = await kv.get(KV.crystals, item.obsId).catch(() => null);
+ if (entry) highOrderItems.push({ tier: "crystal", id: entry.id, data: entry });
+ } else if (item.obsId.startsWith("ins_")) {
+ const entry = await kv.get(KV.insights, item.obsId).catch(() => null);
+ if (entry && !entry.deleted) highOrderItems.push({ tier: "insight", id: entry.id, data: entry });
+ } else {
+ obsItems.push(item);
+ }
+ }
+
const expanded: Array<{
obsId: string;
sessionId: string;
@@ -64,7 +89,7 @@ export function registerSmartSearchFunction(
}> = [];
const results = await Promise.all(
- items.map(({ obsId, sessionId }) =>
+ obsItems.map(({ obsId, sessionId }) =>
findObservation(kv, obsId, sessionId).then((obs) =>
obs ? { obsId, sessionId: obs.sessionId, observation: obs } : null,
),
@@ -87,11 +112,12 @@ export function registerSmartSearchFunction(
logger.info("Smart search expanded", {
requested: data.expandIds.length,
attempted: raw.length,
- returned: scoped.length,
+ returned: scoped.length + highOrderItems.length,
filteredOutOfScope: expanded.length - scoped.length,
+ highOrderExpanded: highOrderItems.length,
truncated,
});
- return { mode: "expanded", results: scoped, truncated };
+ return { mode: "expanded", results: scoped, highOrder: highOrderItems, truncated };
}
if (!data.query || typeof data.query !== "string" || !data.query.trim()) {
@@ -99,28 +125,39 @@ export function registerSmartSearchFunction(
}
const limit = Math.max(1, Math.min(data.limit ?? 20, 100));
- // Lesson recall stays capped: lessons are denser than raw
- // observations so 10 covers most recall flows.
const lessonLimit = Math.min(limit, 10);
const includeLessons = data.includeLessons !== false;
- // Over-fetch when filtering. Hybrid search can't filter on
- // agentId (BM25/vector indexes don't carry it), so we ask the
- // searcher for more hits than we need and trim post-filter. 3×
- // is a defensible middle ground: enough headroom for a small
- // workload, capped at 300 so a 100-limit request never asks for
- // thousands of hits.
+ const includeHighOrder = data.includeHighOrder !== false
+ && isHighOrderSearchEnabled()
+ && !filterAgentId;
+
const overFetchLimit = filterAgentId
? Math.min(limit * 3, 300)
: limit;
- const [hybridResults, lessons] = await Promise.all([
+ const [hybridResults, lessons, highOrderResponse] = await Promise.all([
searchFn(data.query, overFetchLimit),
includeLessons
? recallLessons(sdk, data.query, lessonLimit, data.project)
: Promise.resolve([]),
+ includeHighOrder
+ ? searchHighOrderTiers(kv, data.query, {
+ confidenceFloor: getHighOrderConfidenceFloor(),
+ project: data.project,
+ limit: Math.min(limit, 20),
+ })
+ : Promise.resolve({ results: [], needsBackfill: false }),
]);
+ const highOrderResults = Array.isArray(highOrderResponse) ? highOrderResponse : highOrderResponse.results;
+ const needsBackfill = Array.isArray(highOrderResponse) ? false : highOrderResponse.needsBackfill;
+
+ if (needsBackfill) {
+ // Trigger background backfill fire-and-forget
+ sdk.trigger({ function_id: "mem::backfill-embeddings::high-order", payload: {} }).catch(() => {});
+ }
+
const filteredHybrid = filterAgentId
? hybridResults
.filter((r) => r.observation.agentId === filterAgentId)
@@ -145,13 +182,18 @@ export function registerSmartSearchFunction(
query: data.query,
results: compact.length,
lessons: lessons.length,
+ highOrder: highOrderResults.length,
});
const response: {
mode: "compact";
results: CompactSearchResult[];
lessons?: CompactLessonResult[];
+ highOrder?: CompactHighOrderResult[];
} = { mode: "compact", results: compact };
if (includeLessons) response.lessons = lessons;
+ if (includeHighOrder && highOrderResults.length > 0) {
+ response.highOrder = highOrderResults;
+ }
return response;
},
);
diff --git a/src/index.ts b/src/index.ts
index d20d4693..93914620 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -12,6 +12,7 @@ import {
isConsolidationEnabled,
isContextInjectionEnabled,
isDropStaleIndexEnabled,
+ isHighOrderSearchEnabled,
} from "./config.js";
import {
createProvider,
@@ -249,6 +250,7 @@ async function main() {
registerPatternsFunction(sdk, kv);
registerRememberFunction(sdk, kv);
registerEvictFunction(sdk, kv);
+ registerHighOrderBackfillFunction(sdk, kv);
registerRelationsFunction(sdk, kv);
registerTimelineFunction(sdk, kv);
@@ -272,6 +274,7 @@ async function main() {
registerConsolidationPipelineFunction(sdk, kv, provider);
bootLog(`Consolidation pipeline: registered (CONSOLIDATION_ENABLED=${isConsolidationEnabled() ? "true" : "false"})`);
+ bootLog(`High-order search: ${isHighOrderSearchEnabled() ? "enabled" : "disabled"} (AGENTMEMORY_HIGH_ORDER_SEARCH)`);
if (isAutoCompressEnabled()) {
bootLog(
@@ -516,7 +519,7 @@ async function main() {
`Ready. ${embeddingProvider ? "Triple-stream (BM25+Vector+Graph)" : "BM25+Graph"} search active.`,
);
bootLog(
- `REST API: 125 endpoints at http://localhost:${config.restPort}/agentmemory/*`,
+ `REST API: 126 endpoints at http://localhost:${config.restPort}/agentmemory/*`,
);
bootLog(
`MCP surface (opt-in via \`npx @agentmemory/mcp\`): ${getAllTools().length} tools · 6 resources · 3 prompts`,
diff --git a/src/mcp/server.ts b/src/mcp/server.ts
index e2144567..98287630 100644
--- a/src/mcp/server.ts
+++ b/src/mcp/server.ts
@@ -269,6 +269,7 @@ export function registerMcpEndpoints(
query: args.query,
expandIds,
limit,
+ includeHighOrder: args.includeHighOrder !== "false",
},
});
return {
diff --git a/src/mcp/tools-registry.ts b/src/mcp/tools-registry.ts
index c86d899b..fec18e2d 100644
--- a/src/mcp/tools-registry.ts
+++ b/src/mcp/tools-registry.ts
@@ -127,9 +127,13 @@ export const CORE_TOOLS: McpToolDef[] = [
query: { type: "string", description: "Search query" },
expandIds: {
type: "string",
- description: "Comma-separated observation IDs to expand",
+ description: "Comma-separated IDs to expand (observations, sem_*, proc_*, crys_*, ins_*)",
},
limit: { type: "number", description: "Max results (default 10)" },
+ includeHighOrder: {
+ type: "string",
+ description: "Set to 'false' to exclude high-order memory tiers (semantic, procedural, crystals, insights) from results. Default: enabled.",
+ },
},
required: ["query"],
},
diff --git a/src/state/vector-index.ts b/src/state/vector-index.ts
index d4b8bda7..d22184b6 100644
--- a/src/state/vector-index.ts
+++ b/src/state/vector-index.ts
@@ -5,13 +5,13 @@
// pool. Same risk on the encode side if the input Float32Array is itself
// a sliced view. Reported as a phantom "2048 dimensions on disk" crash
// in #455 / #469 / #584 / #587.
-function float32ToBase64(arr: Float32Array): string {
+export function float32ToBase64(arr: Float32Array): string {
return Buffer.from(arr.buffer, arr.byteOffset, arr.byteLength).toString(
"base64",
);
}
-function base64ToFloat32(b64: string): Float32Array {
+export function base64ToFloat32(b64: string): Float32Array {
const buf = Buffer.from(b64, "base64");
return new Float32Array(
buf.buffer,
diff --git a/src/triggers/api.ts b/src/triggers/api.ts
index bbec4fcd..a9fd2467 100644
--- a/src/triggers/api.ts
+++ b/src/triggers/api.ts
@@ -1107,7 +1107,19 @@ export function registerApiTriggers(
body: { error: "query or expandIds is required" },
};
}
- const result = await sdk.trigger({ function_id: "mem::smart-search", payload: req.body });
+ const body = req.body as Record;
+ const result = await sdk.trigger({
+ function_id: "mem::smart-search",
+ payload: {
+ ...(body.query !== undefined && { query: body.query }),
+ ...(body.expandIds !== undefined && { expandIds: body.expandIds }),
+ ...(body.limit !== undefined && { limit: body.limit }),
+ ...(body.project !== undefined && { project: body.project }),
+ ...(body.includeLessons !== undefined && { includeLessons: body.includeLessons }),
+ ...(body.includeHighOrder !== undefined && { includeHighOrder: body.includeHighOrder }),
+ ...(body.agentId !== undefined && { agentId: body.agentId }),
+ },
+ });
return { status_code: 200, body: result };
},
);
@@ -1117,6 +1129,20 @@ export function registerApiTriggers(
config: { api_path: "/agentmemory/smart-search", http_method: "POST" },
});
+ sdk.registerFunction("api::backfill-high-order",
+ async (req: ApiRequest): Promise => {
+ const authErr = checkAuth(req, secret);
+ if (authErr) return authErr;
+ const result = await sdk.trigger({ function_id: "mem::backfill-embeddings::high-order", payload: {} });
+ return { status_code: 200, body: result };
+ },
+ );
+ sdk.registerTrigger({
+ type: "http",
+ function_id: "api::backfill-high-order",
+ config: { api_path: "/agentmemory/backfill/high-order", http_method: "POST" },
+ });
+
sdk.registerFunction("api::timeline",
async (
req: ApiRequest<{
diff --git a/src/types.ts b/src/types.ts
index b734a4d2..c0fc370d 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -268,6 +268,7 @@ export interface CompactSearchResult {
type: ObservationType;
score: number;
timestamp: string;
+ tier?: "observation";
}
export interface CompactLessonResult {
@@ -280,6 +281,18 @@ export interface CompactLessonResult {
tags: string[];
}
+export type HighOrderTier = "semantic" | "procedural" | "crystal" | "insight";
+
+export interface CompactHighOrderResult {
+ id: string;
+ tier: HighOrderTier;
+ content: string;
+ score: number;
+ confidence: number;
+ project?: string;
+ createdAt: string;
+}
+
export interface TimelineEntry {
observation: CompressedObservation;
sessionId: string;
@@ -457,6 +470,8 @@ export interface SemanticMemory {
strength: number;
createdAt: string;
updatedAt: string;
+ embedding?: string;
+ embeddingModel?: string;
}
export interface ProceduralMemory {
@@ -473,6 +488,8 @@ export interface ProceduralMemory {
strength: number;
createdAt: string;
updatedAt: string;
+ embedding?: string;
+ embeddingModel?: string;
}
export interface TeamConfig {
@@ -744,6 +761,8 @@ export interface Crystal {
sessionId?: string;
project?: string;
createdAt: string;
+ embedding?: string;
+ embeddingModel?: string;
}
export interface Lesson {
@@ -782,6 +801,8 @@ export interface Insight {
lastDecayedAt?: string;
decayRate: number;
deleted?: boolean;
+ embedding?: string;
+ embeddingModel?: string;
}
export interface DiagnosticCheck {
diff --git a/test/high-order-search.test.ts b/test/high-order-search.test.ts
new file mode 100644
index 00000000..930ce709
--- /dev/null
+++ b/test/high-order-search.test.ts
@@ -0,0 +1,353 @@
+import { describe, it, expect } from "vitest";
+import { searchHighOrderTiers } from "../src/functions/high-order-search.js";
+import type {
+ SemanticMemory,
+ ProceduralMemory,
+ Crystal,
+ Insight,
+} from "../src/types.js";
+
+function mockKV() {
+ const store = new Map>();
+ return {
+ get: async (scope: string, key: string): Promise => {
+ return (store.get(scope)?.get(key) as T) ?? null;
+ },
+ set: async (scope: string, key: string, data: T): Promise => {
+ if (!store.has(scope)) store.set(scope, new Map());
+ store.get(scope)!.set(key, data);
+ return data;
+ },
+ delete: async (scope: string, key: string): Promise => {
+ store.get(scope)?.delete(key);
+ },
+ list: async (scope: string): Promise => {
+ const entries = store.get(scope);
+ return entries ? (Array.from(entries.values()) as T[]) : [];
+ },
+ };
+}
+
+function makeSemantic(overrides: Partial = {}): SemanticMemory {
+ return {
+ id: "sem_1",
+ fact: "TypeScript uses structural typing",
+ confidence: 0.9,
+ sourceSessionIds: [],
+ sourceMemoryIds: [],
+ accessCount: 1,
+ lastAccessedAt: new Date().toISOString(),
+ strength: 0.8,
+ createdAt: new Date().toISOString(),
+ updatedAt: new Date().toISOString(),
+ ...overrides,
+ };
+}
+
+function makeProcedural(overrides: Partial = {}): ProceduralMemory {
+ return {
+ id: "proc_1",
+ name: "Run vitest tests",
+ steps: ["npm run build", "npx vitest run"],
+ triggerCondition: "when testing code changes",
+ frequency: 3,
+ sourceSessionIds: [],
+ strength: 0.7,
+ createdAt: new Date().toISOString(),
+ updatedAt: new Date().toISOString(),
+ ...overrides,
+ };
+}
+
+function makeCrystal(overrides: Partial = {}): Crystal {
+ return {
+ id: "crys_1",
+ narrative: "Database migration completed successfully with zero downtime",
+ keyOutcomes: ["zero downtime", "all tables migrated"],
+ filesAffected: ["db/migrate.ts"],
+ lessons: ["always run migrations in a transaction"],
+ sourceActionIds: [],
+ project: "myproject",
+ createdAt: new Date().toISOString(),
+ ...overrides,
+ };
+}
+
+function makeInsight(overrides: Partial = {}): Insight {
+ return {
+ id: "ins_1",
+ title: "TypeScript patterns improve code quality",
+ content: "Using strict TypeScript patterns consistently reduces bugs",
+ confidence: 0.85,
+ reinforcements: 2,
+ sourceConceptCluster: ["typescript", "patterns"],
+ sourceMemoryIds: [],
+ sourceLessonIds: [],
+ sourceCrystalIds: [],
+ tags: ["typescript", "quality"],
+ createdAt: new Date().toISOString(),
+ updatedAt: new Date().toISOString(),
+ decayRate: 0.02,
+ ...overrides,
+ };
+}
+
+describe("searchHighOrderTiers", () => {
+ it("returns semantic facts matching query", async () => {
+ const kv = mockKV();
+ await kv.set("mem:semantic", "sem_1", makeSemantic());
+
+ const { results } = await searchHighOrderTiers(kv as any, "TypeScript typing", {
+ confidenceFloor: 0.3,
+ });
+
+ expect(results.length).toBeGreaterThan(0);
+ expect(results[0].tier).toBe("semantic");
+ expect(results[0].id).toBe("sem_1");
+ });
+
+ it("returns procedural skills matching query", async () => {
+ const kv = mockKV();
+ await kv.set("mem:procedural", "proc_1", makeProcedural());
+
+ const { results } = await searchHighOrderTiers(kv as any, "vitest tests", {
+ confidenceFloor: 0.3,
+ });
+
+ expect(results.length).toBeGreaterThan(0);
+ expect(results[0].tier).toBe("procedural");
+ });
+
+ it("returns crystals matching query", async () => {
+ const kv = mockKV();
+ await kv.set("mem:crystals", "crys_1", makeCrystal());
+
+ const { results } = await searchHighOrderTiers(kv as any, "database migration", {
+ confidenceFloor: 0.3,
+ });
+
+ expect(results.length).toBeGreaterThan(0);
+ expect(results[0].tier).toBe("crystal");
+ expect(results[0].confidence).toBe(1.0);
+ });
+
+ it("returns insights matching query", async () => {
+ const kv = mockKV();
+ await kv.set("mem:insights", "ins_1", makeInsight());
+
+ const { results } = await searchHighOrderTiers(kv as any, "TypeScript patterns", {
+ confidenceFloor: 0.3,
+ });
+
+ expect(results.length).toBeGreaterThan(0);
+ expect(results[0].tier).toBe("insight");
+ });
+
+ it("filters by confidence floor", async () => {
+ const kv = mockKV();
+ await kv.set(
+ "mem:semantic",
+ "sem_low",
+ makeSemantic({ id: "sem_low", confidence: 0.2, strength: 0.9, fact: "test fact" }),
+ );
+ await kv.set(
+ "mem:semantic",
+ "sem_high",
+ makeSemantic({ id: "sem_high", confidence: 0.9, strength: 0.9, fact: "test fact" }),
+ );
+
+ const { results } = await searchHighOrderTiers(kv as any, "test fact", {
+ confidenceFloor: 0.5,
+ });
+
+ expect(results.length).toBe(1);
+ expect(results[0].id).toBe("sem_high");
+ });
+
+ it("uses min(confidence, strength) for semantic decay interaction", async () => {
+ const kv = mockKV();
+ // High confidence but decayed strength
+ await kv.set(
+ "mem:semantic",
+ "sem_1",
+ makeSemantic({ id: "sem_decayed", confidence: 0.9, strength: 0.1, fact: "test fact" }),
+ );
+
+ const { results } = await searchHighOrderTiers(kv as any, "test fact", {
+ confidenceFloor: 0.5,
+ });
+
+ expect(results.length).toBe(0);
+ });
+
+ it("filters procedural by strength", async () => {
+ const kv = mockKV();
+ await kv.set(
+ "mem:procedural",
+ "proc_1",
+ makeProcedural({ id: "proc_weak", strength: 0.2, name: "test test" }),
+ );
+
+ const { results } = await searchHighOrderTiers(kv as any, "test test", {
+ confidenceFloor: 0.5,
+ });
+
+ expect(results.length).toBe(0);
+ });
+
+ it("skips deleted insights", async () => {
+ const kv = mockKV();
+ await kv.set(
+ "mem:insights",
+ "ins_1",
+ makeInsight({ id: "ins_del", title: "test fact", deleted: true }),
+ );
+
+ const { results } = await searchHighOrderTiers(kv as any, "test fact", {
+ confidenceFloor: 0.3,
+ });
+
+ expect(results.length).toBe(0);
+ });
+
+ it("filters crystals by project", async () => {
+ const kv = mockKV();
+ await kv.set("mem:crystals", "crys_a", makeCrystal({ id: "crys_a", project: "projA" }));
+ await kv.set("mem:crystals", "crys_b", makeCrystal({ id: "crys_b", project: "projB" }));
+
+ const { results } = await searchHighOrderTiers(kv as any, "database migration", {
+ confidenceFloor: 0.3,
+ project: "projA",
+ });
+
+ expect(results.length).toBe(1);
+ expect(results[0].id).toBe("crys_a");
+ });
+
+ it("filters insights by project", async () => {
+ const kv = mockKV();
+ await kv.set("mem:insights", "ins_a", makeInsight({ id: "ins_a", project: "projA" }));
+ await kv.set("mem:insights", "ins_b", makeInsight({ id: "ins_b", project: "projB" }));
+
+ const { results } = await searchHighOrderTiers(kv as any, "TypeScript patterns", {
+ confidenceFloor: 0.3,
+ project: "projA",
+ });
+
+ expect(results.length).toBe(1);
+ expect(results[0].id).toBe("ins_a");
+ });
+
+ it("returns empty array for empty query terms", async () => {
+ const kv = mockKV();
+ await kv.set("mem:semantic", "sem_1", makeSemantic());
+
+ const response = await searchHighOrderTiers(kv as any, "a", {
+ confidenceFloor: 0.3,
+ });
+
+ expect(response).toEqual({ results: [], needsBackfill: false });
+ });
+
+ it("returns empty array when no tiers have data", async () => {
+ const kv = mockKV();
+
+ const response = await searchHighOrderTiers(kv as any, "test query", {
+ confidenceFloor: 0.3,
+ });
+
+ expect(response).toEqual({ results: [], needsBackfill: false });
+ });
+
+ it("caps per-tier results at 50", async () => {
+ const kv = mockKV();
+ for (let i = 0; i < 60; i++) {
+ await kv.set(
+ "mem:semantic",
+ `sem_${i}`,
+ makeSemantic({ fact: `test fact ${i}` }),
+ );
+ }
+
+ const { results } = await searchHighOrderTiers(kv as any, "test fact", {
+ confidenceFloor: 0.3,
+ limit: 100,
+ });
+
+ const semanticCount = results.filter((r) => r.tier === "semantic").length;
+ expect(semanticCount).toBeLessThanOrEqual(50);
+ });
+
+ it("applies confidence boost for high-confidence entries", async () => {
+ const kv = mockKV();
+ await kv.set(
+ "mem:semantic",
+ "sem_low",
+ makeSemantic({ id: "sem_low", fact: "test fact", confidence: 0.5, strength: 1.0 }),
+ );
+ await kv.set(
+ "mem:semantic",
+ "sem_high",
+ makeSemantic({ id: "sem_high", fact: "test fact", confidence: 0.95, strength: 1.0 }),
+ );
+
+ const { results } = await searchHighOrderTiers(kv as any, "test fact", {
+ confidenceFloor: 0.3,
+ });
+
+ expect(results.length).toBe(2);
+ expect(results[0].id).toBe("sem_high");
+ expect(results[0].score).toBeGreaterThan(results[1].score);
+ });
+
+ it("respects limit parameter", async () => {
+ const kv = mockKV();
+ for (let i = 0; i < 5; i++) {
+ await kv.set(
+ "mem:semantic",
+ `sem_${i}`,
+ makeSemantic({ id: `sem_${i}`, fact: `test fact ${i}` }),
+ );
+ }
+
+ const { results } = await searchHighOrderTiers(kv as any, "test fact", {
+ confidenceFloor: 0.3,
+ limit: 3,
+ });
+
+ expect(results.length).toBe(3);
+ });
+
+ it("searches across multiple tiers simultaneously", async () => {
+ const kv = mockKV();
+ await kv.set("mem:semantic", "sem_1", makeSemantic({ fact: "shared keyword" }));
+ await kv.set("mem:procedural", "proc_1", makeProcedural({ name: "shared keyword" }));
+
+ const { results } = await searchHighOrderTiers(kv as any, "shared keyword", {
+ confidenceFloor: 0.3,
+ });
+
+ const tiers = new Set(results.map((r) => r.tier));
+ expect(tiers.size).toBeGreaterThanOrEqual(2);
+ expect(tiers.has("semantic")).toBe(true);
+ expect(tiers.has("procedural")).toBe(true);
+ });
+
+ it("truncates long content preview", async () => {
+ const kv = mockKV();
+ const longFact = "a".repeat(300) + " test fact";
+ await kv.set(
+ "mem:semantic",
+ "sem_1",
+ makeSemantic({ fact: longFact }),
+ );
+
+ const { results } = await searchHighOrderTiers(kv as any, "test fact", {
+ confidenceFloor: 0.3,
+ });
+
+ expect(results.length).toBe(1);
+ expect(results[0].content.length).toBeLessThanOrEqual(241); // 240 + ellipsis
+ expect(results[0].content.endsWith("…")).toBe(true);
+ });
+});
diff --git a/test/smart-search.test.ts b/test/smart-search.test.ts
index 9d0c94e0..37354564 100644
--- a/test/smart-search.test.ts
+++ b/test/smart-search.test.ts
@@ -4,6 +4,17 @@ vi.mock("../src/logger.js", () => ({
logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() },
}));
+vi.mock("../src/config.js", async (importOriginal) => {
+ const actual = await importOriginal() as Record;
+ return {
+ ...actual,
+ isHighOrderSearchEnabled: vi.fn().mockReturnValue(false),
+ getHighOrderConfidenceFloor: vi.fn().mockReturnValue(0.3),
+ isAgentScopeIsolated: vi.fn().mockReturnValue(false),
+ getAgentId: vi.fn().mockReturnValue(undefined),
+ };
+});
+
import { registerSmartSearchFunction } from "../src/functions/smart-search.js";
import type {
CompressedObservation,
@@ -291,4 +302,167 @@ describe("Smart Search Function", () => {
expect(result.lessons).toEqual([]);
});
});
+
+ describe("high-order tier integration", () => {
+ beforeEach(async () => {
+ const { isHighOrderSearchEnabled, getHighOrderConfidenceFloor } = await import("../src/config.js");
+ vi.mocked(isHighOrderSearchEnabled).mockReturnValue(true);
+ vi.mocked(getHighOrderConfidenceFloor).mockReturnValue(0.3);
+
+ sdk.registerFunction("mem::lesson-recall", async () => ({
+ success: true,
+ lessons: [],
+ }));
+ });
+
+ it("compact mode includes highOrder results when enabled", async () => {
+ await kv.set("mem:semantic", "sem_1", {
+ id: "sem_1",
+ fact: "auth handler uses JWT tokens for authentication",
+ confidence: 0.9,
+ strength: 0.8,
+ sourceSessionIds: [],
+ sourceMemoryIds: [],
+ accessCount: 1,
+ lastAccessedAt: "2026-01-01T00:00:00Z",
+ createdAt: "2026-01-01T00:00:00Z",
+ updatedAt: "2026-01-01T00:00:00Z",
+ });
+
+ const result = (await sdk.trigger("mem::smart-search", {
+ query: "auth",
+ })) as { results: any[]; highOrder?: any[] };
+
+ expect(result.highOrder).toBeDefined();
+ expect(result.highOrder!.length).toBeGreaterThan(0);
+ expect(result.highOrder![0].tier).toBe("semantic");
+ });
+
+ it("includeHighOrder=false excludes high-order results", async () => {
+ await kv.set("mem:semantic", "sem_1", {
+ id: "sem_1",
+ fact: "auth handler uses JWT tokens",
+ confidence: 0.9,
+ strength: 0.8,
+ sourceSessionIds: [],
+ sourceMemoryIds: [],
+ accessCount: 1,
+ lastAccessedAt: "2026-01-01T00:00:00Z",
+ createdAt: "2026-01-01T00:00:00Z",
+ updatedAt: "2026-01-01T00:00:00Z",
+ });
+
+ const result = (await sdk.trigger("mem::smart-search", {
+ query: "auth",
+ includeHighOrder: false,
+ })) as { results: any[]; highOrder?: any[] };
+
+ expect(result.highOrder).toBeUndefined();
+ });
+
+ it("empty high-order tiers produce no highOrder field", async () => {
+ const result = (await sdk.trigger("mem::smart-search", {
+ query: "something unique with no match",
+ })) as { results: any[]; highOrder?: any[] };
+
+ expect(result.highOrder).toBeUndefined();
+ });
+
+ it("expandIds dispatches sem_ prefix to semantic KV", async () => {
+ await kv.set("mem:semantic", "sem_test", {
+ id: "sem_test",
+ fact: "test semantic fact",
+ confidence: 0.9,
+ strength: 0.8,
+ sourceSessionIds: [],
+ sourceMemoryIds: [],
+ accessCount: 1,
+ lastAccessedAt: "2026-01-01T00:00:00Z",
+ createdAt: "2026-01-01T00:00:00Z",
+ updatedAt: "2026-01-01T00:00:00Z",
+ });
+
+ const result = (await sdk.trigger("mem::smart-search", {
+ query: "test",
+ expandIds: ["sem_test"],
+ })) as { mode: string; results: any[]; highOrder?: any[] };
+
+ expect(result.mode).toBe("expanded");
+ expect(result.highOrder).toBeDefined();
+ expect(result.highOrder!.length).toBe(1);
+ expect(result.highOrder![0].tier).toBe("semantic");
+ expect(result.highOrder![0].id).toBe("sem_test");
+ });
+
+ it("expandIds dispatches crys_ prefix to crystals KV", async () => {
+ await kv.set("mem:crystals", "crys_test", {
+ id: "crys_test",
+ narrative: "test crystal narrative",
+ keyOutcomes: ["outcome1"],
+ filesAffected: [],
+ lessons: ["lesson1"],
+ sourceActionIds: [],
+ project: "test-project",
+ createdAt: "2026-01-01T00:00:00Z",
+ });
+
+ const result = (await sdk.trigger("mem::smart-search", {
+ query: "test",
+ expandIds: ["crys_test"],
+ })) as { mode: string; results: any[]; highOrder?: any[] };
+
+ expect(result.highOrder!.length).toBe(1);
+ expect(result.highOrder![0].tier).toBe("crystal");
+ });
+
+ it("expandIds skips deleted insights", async () => {
+ await kv.set("mem:insights", "ins_del", {
+ id: "ins_del",
+ title: "deleted insight",
+ content: "should be skipped",
+ confidence: 0.9,
+ reinforcements: 1,
+ sourceConceptCluster: [],
+ sourceMemoryIds: [],
+ sourceLessonIds: [],
+ sourceCrystalIds: [],
+ tags: [],
+ createdAt: "2026-01-01T00:00:00Z",
+ updatedAt: "2026-01-01T00:00:00Z",
+ decayRate: 0.02,
+ deleted: true,
+ });
+
+ const result = (await sdk.trigger("mem::smart-search", {
+ query: "test",
+ expandIds: ["ins_del"],
+ })) as { mode: string; results: any[]; highOrder?: any[] };
+
+ expect(result.highOrder!.length).toBe(0);
+ });
+
+ it("mixed expandIds returns both observations and high-order", async () => {
+ await kv.set("mem:semantic", "sem_mix", {
+ id: "sem_mix",
+ fact: "mixed test fact",
+ confidence: 0.9,
+ strength: 0.8,
+ sourceSessionIds: [],
+ sourceMemoryIds: [],
+ accessCount: 1,
+ lastAccessedAt: "2026-01-01T00:00:00Z",
+ createdAt: "2026-01-01T00:00:00Z",
+ updatedAt: "2026-01-01T00:00:00Z",
+ });
+
+ const result = (await sdk.trigger("mem::smart-search", {
+ query: "test",
+ expandIds: ["obs_1", "sem_mix"],
+ })) as { mode: string; results: any[]; highOrder?: any[] };
+
+ expect(result.mode).toBe("expanded");
+ expect(result.results.length).toBe(1);
+ expect(result.highOrder!.length).toBe(1);
+ });
+ });
});