From 4a2151bcd49fa4b3120fa3da021133fad809268b Mon Sep 17 00:00:00 2001 From: RerankerGuo <121015044+RerankerGuo@users.noreply.github.com> Date: Wed, 5 Aug 2026 16:00:05 +0800 Subject: [PATCH 1/2] fix(plugin): derive missing crystallization summaries --- .../core/skill/crystallize.ts | 15 ++++++- .../tests/unit/skill/crystallize.test.ts | 41 +++++++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/apps/memos-local-plugin/core/skill/crystallize.ts b/apps/memos-local-plugin/core/skill/crystallize.ts index c45c5453d..d891815db 100644 --- a/apps/memos-local-plugin/core/skill/crystallize.ts +++ b/apps/memos-local-plugin/core/skill/crystallize.ts @@ -353,11 +353,24 @@ function normaliseDraft( const displayTitle = sanitizeDerivedText(raw.display_title ?? raw.displayTitle ?? input.policy.title ?? name) || name; - const summary = sanitizeDerivedText(raw.summary); const parameters = asArray(raw.parameters).map(coerceParameter).filter(Boolean) as SkillParameterDraft[]; const preconditions = sanitizeDerivedMarkdownList(asStringArray(raw.preconditions)); const steps = asArray(raw.steps).map(coerceStep).filter(Boolean) as SkillStepDraft[]; + const summary = + sanitizeDerivedText(raw.summary) || + [ + raw.retrieval_blurb, + raw.retrievalBlurb, + steps[0]?.body, + steps[0]?.title, + displayTitle, + name, + ] + .map(sanitizeDerivedText) + .find((candidate) => candidate.length > 0) + ?.slice(0, 200) || + "skill procedure"; const examples = asArray(raw.examples).map(coerceExample).filter(Boolean) as SkillExampleDraft[]; const tags = dedupeLc(sanitizeDerivedList(asStringArray(raw.tags))); // V7 §2.4.6 — coerce both `decision_guidance` (preferred LLM key) diff --git a/apps/memos-local-plugin/tests/unit/skill/crystallize.test.ts b/apps/memos-local-plugin/tests/unit/skill/crystallize.test.ts index a2e0e63eb..68fc6adc1 100644 --- a/apps/memos-local-plugin/tests/unit/skill/crystallize.test.ts +++ b/apps/memos-local-plugin/tests/unit/skill/crystallize.test.ts @@ -120,6 +120,47 @@ describe("skill/crystallize", () => { expect(r.draft.tools).toEqual(["shell", "pip.install"]); }); + it("derives a summary from the first step when the LLM omits it", async () => { + const draft = makeDraft({ + steps: [{ title: "Inspect failure", body: "Read the package install error." }], + }) as unknown as Record; + delete draft.summary; + const llm = fakeLlm({ + completeJson: { "skill.crystallize": draft }, + }); + + const r = await crystallizeDraft( + { policy: mkPolicy(), evidence: [mkTrace("tr_1", "pip fails")], namingSpace: [] }, + { llm, log, config: makeSkillConfig(), validate: defaultDraftValidator }, + ); + + expect(r.ok).toBe(true); + if (!r.ok) return; + expect(r.draft.summary).toBe("Read the package install error."); + }); + + it("prefers a retrieval blurb when deriving a missing summary", async () => { + const draft = makeDraft({ + steps: [{ title: "Inspect failure", body: "Read the package install error." }], + }) as unknown as Record; + delete draft.summary; + draft.retrieval_blurb = "Use this skill for Alpine package installation failures."; + const llm = fakeLlm({ + completeJson: { "skill.crystallize": draft }, + }); + + const r = await crystallizeDraft( + { policy: mkPolicy(), evidence: [mkTrace("tr_1", "pip fails")], namingSpace: [] }, + { llm, log, config: makeSkillConfig(), validate: defaultDraftValidator }, + ); + + expect(r.ok).toBe(true); + if (!r.ok) return; + expect(r.draft.summary).toBe( + "Use this skill for Alpine package installation failures.", + ); + }); + it("cleans unsafe markup from LLM-derived skill fields", async () => { const policy = mkPolicy(); const llm = fakeLlm({ From a1dcfb2c100544680da866a10bb945f1e06a1e6b Mon Sep 17 00:00:00 2001 From: jiachengzhen Date: Fri, 7 Aug 2026 01:16:49 +0800 Subject: [PATCH 2/2] fix(plugin): clamp crystallization summaries --- .../core/skill/crystallize.ts | 20 +++++++------------ .../tests/unit/skill/crystallize.test.ts | 17 ++++++++++++++++ 2 files changed, 24 insertions(+), 13 deletions(-) diff --git a/apps/memos-local-plugin/core/skill/crystallize.ts b/apps/memos-local-plugin/core/skill/crystallize.ts index d891815db..1a5206bb5 100644 --- a/apps/memos-local-plugin/core/skill/crystallize.ts +++ b/apps/memos-local-plugin/core/skill/crystallize.ts @@ -357,20 +357,14 @@ function normaliseDraft( const parameters = asArray(raw.parameters).map(coerceParameter).filter(Boolean) as SkillParameterDraft[]; const preconditions = sanitizeDerivedMarkdownList(asStringArray(raw.preconditions)); const steps = asArray(raw.steps).map(coerceStep).filter(Boolean) as SkillStepDraft[]; - const summary = + const summarySource = sanitizeDerivedText(raw.summary) || - [ - raw.retrieval_blurb, - raw.retrievalBlurb, - steps[0]?.body, - steps[0]?.title, - displayTitle, - name, - ] - .map(sanitizeDerivedText) - .find((candidate) => candidate.length > 0) - ?.slice(0, 200) || - "skill procedure"; + sanitizeDerivedText(raw.retrieval_blurb) || + sanitizeDerivedText(raw.retrievalBlurb) || + sanitizeDerivedText(steps[0]?.body) || + sanitizeDerivedText(steps[0]?.title) || + displayTitle; + const summary = summarySource.slice(0, 200); const examples = asArray(raw.examples).map(coerceExample).filter(Boolean) as SkillExampleDraft[]; const tags = dedupeLc(sanitizeDerivedList(asStringArray(raw.tags))); // V7 §2.4.6 — coerce both `decision_guidance` (preferred LLM key) diff --git a/apps/memos-local-plugin/tests/unit/skill/crystallize.test.ts b/apps/memos-local-plugin/tests/unit/skill/crystallize.test.ts index 68fc6adc1..e923105b2 100644 --- a/apps/memos-local-plugin/tests/unit/skill/crystallize.test.ts +++ b/apps/memos-local-plugin/tests/unit/skill/crystallize.test.ts @@ -161,6 +161,23 @@ describe("skill/crystallize", () => { ); }); + it("caps an explicit summary at 200 characters", async () => { + const llm = fakeLlm({ + completeJson: { + "skill.crystallize": makeDraft({ summary: "x".repeat(250) }), + }, + }); + + const r = await crystallizeDraft( + { policy: mkPolicy(), evidence: [mkTrace("tr_1", "pip fails")], namingSpace: [] }, + { llm, log, config: makeSkillConfig(), validate: defaultDraftValidator }, + ); + + expect(r.ok).toBe(true); + if (!r.ok) return; + expect(r.draft.summary).toBe("x".repeat(200)); + }); + it("cleans unsafe markup from LLM-derived skill fields", async () => { const policy = mkPolicy(); const llm = fakeLlm({