Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 13 additions & 5 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,21 +66,29 @@ export const LoreConfig = z.object({
* When enabled, the configured model generates 2–3 alternative query phrasings
* before search, improving recall for ambiguous queries. */
queryExpansion: z.boolean().default(false),
/** Vector embedding search via Voyage AI.
* Automatically enabled when VOYAGE_API_KEY env var is set.
/** Vector embedding search.
* Supports multiple providers: "voyage" (Voyage AI, VOYAGE_API_KEY),
* "openai" (OpenAI, OPENAI_API_KEY).
* Automatically enabled when the configured provider's API key env var is set.
* Set enabled: false to explicitly disable even with the key present. */
embeddings: z
.object({
/** Enable/disable vector embedding search. Default: true.
* Set to false to explicitly disable even when VOYAGE_API_KEY is set. */
* Set to false to explicitly disable even when the API key is set. */
enabled: z.boolean().default(true),
/** Voyage AI model ID. Default: voyage-code-3. */
/** Embedding provider. Default: "voyage".
* Each provider reads its own env var for the API key:
* - "voyage": VOYAGE_API_KEY (default model: voyage-code-3, 1024 dims)
* - "openai": OPENAI_API_KEY (default model: text-embedding-3-small, 1536 dims) */
provider: z.enum(["voyage", "openai"]).default("voyage"),
/** Model ID for the embedding provider. Default depends on provider. */
model: z.string().default("voyage-code-3"),
/** Embedding dimensions. Default: 1024. */
dimensions: z.number().min(256).max(2048).default(1024),
})
.default({
enabled: true,
provider: "voyage",
model: "voyage-code-3",
dimensions: 1024,
}),
Expand All @@ -89,7 +97,7 @@ export const LoreConfig = z.object({
ftsWeights: { title: 6.0, content: 2.0, category: 3.0 },
recallLimit: 10,
queryExpansion: false,
embeddings: { enabled: true, model: "voyage-code-3", dimensions: 1024 },
embeddings: { enabled: true, provider: "voyage" as const, model: "voyage-code-3", dimensions: 1024 },
}),
crossProject: z.boolean().default(false),
agentsFile: z
Expand Down
18 changes: 17 additions & 1 deletion src/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Database } from "bun:sqlite";
import { join, dirname } from "path";
import { mkdirSync } from "fs";

const SCHEMA_VERSION = 8;
const SCHEMA_VERSION = 9;

const MIGRATIONS: string[] = [
`
Expand Down Expand Up @@ -220,6 +220,14 @@ const MIGRATIONS: string[] = [
value TEXT NOT NULL
);
`,
`
-- Version 9: Embedding BLOB column for distillation vector search.
-- Same pattern as knowledge embeddings (version 8). Enables semantic
-- search over distilled session summaries via cosine similarity.
-- No backfill — entries get embedded lazily on next distillation
-- or via explicit backfill when embeddings are first enabled.
ALTER TABLE distillations ADD COLUMN embedding BLOB;
`,
];

function dataDir() {
Expand Down Expand Up @@ -316,6 +324,14 @@ export function projectId(path: string): string | undefined {
return row?.id;
}

/** Look up a project's display name by its internal ID. */
export function projectName(id: string): string | null {
const row = db()
.query("SELECT name FROM projects WHERE id = ?")
.get(id) as { name: string } | null;
return row?.name ?? null;
}

/**
* Returns true if Lore has never been used before (no projects in the DB).
* Must be called before ensureProject() to get an accurate result.
Expand Down
16 changes: 14 additions & 2 deletions src/distillation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { createOpencodeClient } from "@opencode-ai/sdk";
import { db, ensureProject } from "./db";
import { config } from "./config";
import * as temporal from "./temporal";
import * as embedding from "./embedding";
import * as log from "./log";
import {
DISTILLATION_SYSTEM,
Expand Down Expand Up @@ -402,14 +403,20 @@ async function distillSegment(input: {
const result = parseDistillationResult(responsePart.text);
if (!result) return null;

storeDistillation({
const distillId = storeDistillation({
projectPath: input.projectPath,
sessionID: input.sessionID,
observations: result.observations,
sourceIDs: input.messages.map((m) => m.id),
generation: 0,
});
temporal.markDistilled(input.messages.map((m) => m.id));

// Fire-and-forget: embed the distillation for vector search
if (embedding.isAvailable()) {
embedding.embedDistillation(distillId, result.observations);
}

return result;
}

Expand Down Expand Up @@ -458,14 +465,19 @@ async function metaDistill(input: {
// Store the meta-distillation at generation N+1
const maxGen = Math.max(...existing.map((d) => d.generation));
const allSourceIDs = existing.flatMap((d) => d.source_ids);
storeDistillation({
const metaId = storeDistillation({
projectPath: input.projectPath,
sessionID: input.sessionID,
observations: result.observations,
sourceIDs: allSourceIDs,
generation: maxGen + 1,
});

// Fire-and-forget: embed the meta-distillation for vector search
if (embedding.isAvailable()) {
embedding.embedDistillation(metaId, result.observations);
}

// Archive the gen-0 distillations that were merged into gen-1+.
// They remain searchable via recall but excluded from the in-context prefix.
archiveDistillations(existing.map((d) => d.id));
Expand Down
Loading
Loading