diff --git a/assistant/src/memory/v2/__tests__/injection.test.ts b/assistant/src/memory/v2/__tests__/injection.test.ts index 2e8efa3c0b1..e57b17c8213 100644 --- a/assistant/src/memory/v2/__tests__/injection.test.ts +++ b/assistant/src/memory/v2/__tests__/injection.test.ts @@ -604,7 +604,6 @@ describe("injectMemoryV2Block", () => { [ { id: "example-skill-a", - displayName: "Example Skill A", content: 'The "Example Skill A" skill (example-skill-a) is available. Helps with examples.', }, @@ -646,7 +645,6 @@ describe("injectMemoryV2Block", () => { [ { id: "example-skill-a", - displayName: "Example Skill A", content: 'The "Example Skill A" skill (example-skill-a) is available. Helps with examples.', }, @@ -706,7 +704,6 @@ describe("injectMemoryV2Block", () => { // skills subsection. const skillEntry = { id: "example-skill-a", - displayName: "Example Skill A", content: 'The "Example Skill A" skill (example-skill-a) is available. Helps with examples.', }; @@ -769,7 +766,6 @@ describe("injectMemoryV2Block", () => { [ { id: "example-skill-a", - displayName: "Example Skill A", content: 'The "Example Skill A" skill (example-skill-a) is available.', }, diff --git a/assistant/src/memory/v2/__tests__/skill-qdrant.test.ts b/assistant/src/memory/v2/__tests__/skill-qdrant.test.ts index 9a19b9490e7..00aadb354bf 100644 --- a/assistant/src/memory/v2/__tests__/skill-qdrant.test.ts +++ b/assistant/src/memory/v2/__tests__/skill-qdrant.test.ts @@ -30,7 +30,6 @@ type MockPoint = { vector: { dense: number[]; sparse: { indices: number[]; values: number[] } }; payload: { id: string; - displayName: string; content: string; updated_at: number; }; @@ -150,7 +149,6 @@ mock.module("@qdrant/js-client-rest", () => ({ const { ensureSkillCollection, upsertSkillEmbedding, - deleteSkillEmbedding, pruneSkillsExcept, hybridQuerySkills, MEMORY_V2_SKILLS_COLLECTION, @@ -271,7 +269,6 @@ describe("memory v2 skill qdrant — upsert", () => { await upsertSkillEmbedding({ id: "example-skill-1", - displayName: "Example Skill 1", content: "The Example Skill 1 (example-skill-1) is available. ...", dense: [0.1, 0.2, 0.3], sparse: { indices: [1, 2], values: [0.5, 0.5] }, @@ -285,7 +282,6 @@ describe("memory v2 skill qdrant — upsert", () => { const [point] = call.points; expect(point.payload).toEqual({ id: "example-skill-1", - displayName: "Example Skill 1", content: "The Example Skill 1 (example-skill-1) is available. ...", updated_at: 1714000000000, }); @@ -305,7 +301,6 @@ describe("memory v2 skill qdrant — upsert", () => { await upsertSkillEmbedding({ id: "example-skill-1", - displayName: "Example Skill 1", content: "first", dense: [0.1], sparse: { indices: [1], values: [1] }, @@ -313,7 +308,6 @@ describe("memory v2 skill qdrant — upsert", () => { }); await upsertSkillEmbedding({ id: "example-skill-1", - displayName: "Example Skill 1", content: "second", dense: [0.9], sparse: { indices: [9], values: [0.5] }, @@ -331,7 +325,6 @@ describe("memory v2 skill qdrant — upsert", () => { await upsertSkillEmbedding({ id: "example-skill-1", - displayName: "Example Skill 1", content: "a", dense: [0.1], sparse: { indices: [1], values: [1] }, @@ -339,7 +332,6 @@ describe("memory v2 skill qdrant — upsert", () => { }); await upsertSkillEmbedding({ id: "example-skill-2", - displayName: "Example Skill 2", content: "b", dense: [0.1], sparse: { indices: [1], values: [1] }, @@ -352,52 +344,6 @@ describe("memory v2 skill qdrant — upsert", () => { }); }); -describe("memory v2 skill qdrant — delete", () => { - beforeEach(resetState); - afterEach(resetState); - - test("deletes a skill by its deterministic point id", async () => { - state.collectionExistsBeforeCreate = true; - - await deleteSkillEmbedding("example-skill-1"); - - expect(state.deleteCalls).toHaveLength(1); - const call = state.deleteCalls[0]; - expect(call.wait).toBe(true); - expect(call.points).toHaveLength(1); - expect(call.points[0]).toMatch( - /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/, - ); - }); - - test("delete is idempotent across repeated calls (no exception)", async () => { - state.collectionExistsBeforeCreate = true; - - await deleteSkillEmbedding("example-skill-1"); - await deleteSkillEmbedding("example-skill-1"); - - expect(state.deleteCalls).toHaveLength(2); - }); - - test("deletes use the same point id as upserts for the same skill id", async () => { - state.collectionExistsBeforeCreate = true; - - await upsertSkillEmbedding({ - id: "example-skill-1", - displayName: "Example Skill 1", - content: "x", - dense: [0.1], - sparse: { indices: [1], values: [1] }, - updatedAt: 1, - }); - await deleteSkillEmbedding("example-skill-1"); - - expect(state.upsertCalls[0].points[0].id).toBe( - String(state.deleteCalls[0].points[0]), - ); - }); -}); - describe("memory v2 skill qdrant — prune", () => { beforeEach(resetState); afterEach(resetState); diff --git a/assistant/src/memory/v2/__tests__/skill-store.test.ts b/assistant/src/memory/v2/__tests__/skill-store.test.ts index 6fc33c9c303..6e6787f82b5 100644 --- a/assistant/src/memory/v2/__tests__/skill-store.test.ts +++ b/assistant/src/memory/v2/__tests__/skill-store.test.ts @@ -37,7 +37,6 @@ interface TestState { sparseReturn: { indices: number[]; values: number[] }; upsertCalls: Array<{ id: string; - displayName: string; content: string; dense: number[]; sparse: { indices: number[]; values: number[] }; @@ -174,7 +173,6 @@ describe("seedV2SkillEntries", () => { // Each upsert carries the per-skill dense + sparse + content payload. const callA = state.upsertCalls.find((c) => c.id === "example-skill-a")!; - expect(callA.displayName).toBe("Skill A"); expect(callA.dense).toEqual([0.1, 0.2, 0.3]); expect(callA.sparse).toEqual(state.sparseReturn); expect(callA.content).toContain("Skill A"); @@ -287,12 +285,11 @@ describe("seedV2SkillEntries", () => { const entryB = getSkillCapability("example-skill-b"); expect(entryA).not.toBeNull(); expect(entryA?.id).toBe("example-skill-a"); - expect(entryA?.displayName).toBe("Skill A"); expect(entryA?.content).toContain("Skill A"); expect(entryB).not.toBeNull(); expect(entryB?.id).toBe("example-skill-b"); - expect(entryB?.displayName).toBe("Skill B"); + expect(entryB?.content).toContain("Skill B"); // Unknown ids return null even when the cache is populated. expect(getSkillCapability("unknown-skill")).toBeNull(); diff --git a/assistant/src/memory/v2/skill-qdrant.ts b/assistant/src/memory/v2/skill-qdrant.ts index 94365219ebb..c890bfb4428 100644 --- a/assistant/src/memory/v2/skill-qdrant.ts +++ b/assistant/src/memory/v2/skill-qdrant.ts @@ -42,8 +42,12 @@ export const MEMORY_V2_SKILLS_COLLECTION = "memory_v2_skills"; */ export const SKILL_NAMESPACE = "f1903e7f-1b9d-4c15-ac46-3540b8b0a9f6"; -/** Per-channel score for a single skill hit returned by hybrid query. */ -export interface SkillQueryResult { +/** + * Per-channel score for a single skill hit returned by hybrid query. + * Module-private — `sim.ts` consumes the fields by duck-typing rather than + * naming the type, so there is no benefit to exporting it. + */ +interface SkillQueryResult { id: string; /** * Dense cosine similarity, when the id appeared in the dense top-`limit`. @@ -155,7 +159,6 @@ export async function ensureSkillCollection(): Promise { */ export async function upsertSkillEmbedding(params: { id: string; - displayName: string; content: string; dense: number[]; sparse: SparseEmbedding; @@ -163,7 +166,7 @@ export async function upsertSkillEmbedding(params: { }): Promise { await ensureSkillCollection(); - const { id, displayName, content, dense, sparse, updatedAt } = params; + const { id, content, dense, sparse, updatedAt } = params; const client = getClient(); const pointId = pointIdForId(id); @@ -176,7 +179,6 @@ export async function upsertSkillEmbedding(params: { vector: { dense, sparse }, payload: { id, - displayName, content, updated_at: updatedAt, }, @@ -197,30 +199,6 @@ export async function upsertSkillEmbedding(params: { } } -/** Remove the embedding for a skill id. Idempotent: no-op when the id is absent. */ -export async function deleteSkillEmbedding(id: string): Promise { - await ensureSkillCollection(); - - const client = getClient(); - const doDelete = () => - client.delete(MEMORY_V2_SKILLS_COLLECTION, { - wait: true, - points: [pointIdForId(id)], - }); - - try { - await doDelete(); - } catch (err) { - if (isCollectionMissing(err)) { - _collectionReady = false; - await ensureSkillCollection(); - await doDelete(); - return; - } - throw err; - } -} - /** * Remove every skill point whose `payload.id` is not in `activeIds`. Used by * `seedV2SkillEntries` to drop stale points after a skill is uninstalled or diff --git a/assistant/src/memory/v2/skill-store.ts b/assistant/src/memory/v2/skill-store.ts index 21a49ad27b6..231c04e15c8 100644 --- a/assistant/src/memory/v2/skill-store.ts +++ b/assistant/src/memory/v2/skill-store.ts @@ -79,7 +79,7 @@ export async function seedV2SkillEntries(): Promise { const augmented = augmentMcpSetupDescription(fromSkillSummary(summary)); const content = buildSkillContent(augmented); - seeds.push({ id: summary.id, displayName: summary.displayName, content }); + seeds.push({ id: summary.id, content }); } // Embed all content strings in one batched call. Sparse vectors are diff --git a/assistant/src/memory/v2/types.ts b/assistant/src/memory/v2/types.ts index e846f5efa72..0ad292341e5 100644 --- a/assistant/src/memory/v2/types.ts +++ b/assistant/src/memory/v2/types.ts @@ -2,10 +2,11 @@ // Memory v2 — Shared types // --------------------------------------------------------------------------- // -// Zod schemas (and inferred TypeScript types) shared across the v2 memory -// subsystem. Each value here crosses a serialization boundary — YAML -// frontmatter, on-disk JSON, or a SQLite JSON column — so runtime validation -// is needed wherever it is read. +// Types shared across the v2 memory subsystem. Most values here cross a +// serialization boundary — YAML frontmatter, on-disk JSON, or a SQLite JSON +// column — so they ship as Zod schemas with inferred TypeScript types so +// runtime validation runs wherever they are read. The skill-autoinjection +// entry stays a plain `interface` because it is purely in-process. // // This file must not import from any other `memory/v2/*` module — it is the // leaf of the v2 dependency graph. @@ -98,28 +99,18 @@ export type ActivationState = z.infer; // --------------------------------------------------------------------------- /** - * Per-skill capability snapshot held in-process and embedded into - * the `memory_v2_skills` Qdrant collection. `content` is the rendered - * `buildSkillContent` string — already capped at 500 chars upstream — and - * is what we embed and what we render in `### Skills You Can Use`. - */ -export const SkillEntrySchema = z.object({ - id: z.string(), - displayName: z.string(), - content: z.string(), -}); - -export type SkillEntry = z.infer; - -/** - * Payload carried alongside each `memory_v2_skills` Qdrant point. Mirrors - * the `ConceptPagePayload` shape but keyed on `id` instead of `slug`. + * Per-skill capability snapshot held in-process and embedded into the + * `memory_v2_skills` Qdrant collection. `content` is the rendered + * `buildSkillContent` string — already capped at 500 chars upstream and + * already containing the skill's display name — and is what we embed and + * what we render verbatim in `### Skills You Can Use`. + * + * Plain interface (no Zod) because skill data does not cross a + * serialization boundary: it is built in-process by `seedV2SkillEntries` + * and read in-process by `renderInjectionBlock`. The Qdrant payload is + * not parsed back through this type. */ -export const SkillEmbeddingPayloadSchema = z.object({ - id: z.string(), - displayName: z.string(), - content: z.string(), - updated_at: z.number().int().nonnegative(), -}); - -export type SkillEmbeddingPayload = z.infer; +export interface SkillEntry { + id: string; + content: string; +}