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
4 changes: 0 additions & 4 deletions assistant/src/memory/v2/__tests__/injection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.',
},
Expand Down Expand Up @@ -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.',
},
Expand Down Expand Up @@ -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.',
};
Expand Down Expand Up @@ -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.',
},
Expand Down
54 changes: 0 additions & 54 deletions assistant/src/memory/v2/__tests__/skill-qdrant.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ type MockPoint = {
vector: { dense: number[]; sparse: { indices: number[]; values: number[] } };
payload: {
id: string;
displayName: string;
content: string;
updated_at: number;
};
Expand Down Expand Up @@ -150,7 +149,6 @@ mock.module("@qdrant/js-client-rest", () => ({
const {
ensureSkillCollection,
upsertSkillEmbedding,
deleteSkillEmbedding,
pruneSkillsExcept,
hybridQuerySkills,
MEMORY_V2_SKILLS_COLLECTION,
Expand Down Expand Up @@ -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] },
Expand All @@ -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,
});
Expand All @@ -305,15 +301,13 @@ 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] },
updatedAt: 1,
});
await upsertSkillEmbedding({
id: "example-skill-1",
displayName: "Example Skill 1",
content: "second",
dense: [0.9],
sparse: { indices: [9], values: [0.5] },
Expand All @@ -331,15 +325,13 @@ 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] },
updatedAt: 1,
});
await upsertSkillEmbedding({
id: "example-skill-2",
displayName: "Example Skill 2",
content: "b",
dense: [0.1],
sparse: { indices: [1], values: [1] },
Expand All @@ -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);
Expand Down
5 changes: 1 addition & 4 deletions assistant/src/memory/v2/__tests__/skill-store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[] };
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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();
Expand Down
36 changes: 7 additions & 29 deletions assistant/src/memory/v2/skill-qdrant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down Expand Up @@ -155,15 +159,14 @@ export async function ensureSkillCollection(): Promise<void> {
*/
export async function upsertSkillEmbedding(params: {
id: string;
displayName: string;
content: string;
dense: number[];
sparse: SparseEmbedding;
updatedAt: number;
}): Promise<void> {
await ensureSkillCollection();

const { id, displayName, content, dense, sparse, updatedAt } = params;
const { id, content, dense, sparse, updatedAt } = params;
const client = getClient();
const pointId = pointIdForId(id);

Expand All @@ -176,7 +179,6 @@ export async function upsertSkillEmbedding(params: {
vector: { dense, sparse },
payload: {
id,
displayName,
content,
updated_at: updatedAt,
},
Expand All @@ -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<void> {
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
Expand Down
2 changes: 1 addition & 1 deletion assistant/src/memory/v2/skill-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ export async function seedV2SkillEntries(): Promise<void> {

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
Expand Down
47 changes: 19 additions & 28 deletions assistant/src/memory/v2/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -98,28 +99,18 @@ export type ActivationState = z.infer<typeof ActivationStateSchema>;
// ---------------------------------------------------------------------------

/**
* 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<typeof SkillEntrySchema>;

/**
* 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<typeof SkillEmbeddingPayloadSchema>;
export interface SkillEntry {
id: string;
content: string;
}
Loading