From 1a7e9bc9c87c1b4c620a3a0e126c59e422f48921 Mon Sep 17 00:00:00 2001 From: 34262315716 <34262315716@users.noreply.github.com> Date: Mon, 6 Jul 2026 20:25:26 +0800 Subject: [PATCH 1/3] fix(skill-crystallize): retry with context when LLM returns empty response --- .../core/skill/crystallize.ts | 74 ++++++++++++++++++- 1 file changed, 73 insertions(+), 1 deletion(-) diff --git a/apps/memos-local-plugin/core/skill/crystallize.ts b/apps/memos-local-plugin/core/skill/crystallize.ts index fd6a70c67..99516f736 100644 --- a/apps/memos-local-plugin/core/skill/crystallize.ts +++ b/apps/memos-local-plugin/core/skill/crystallize.ts @@ -9,7 +9,7 @@ * traces we fail fast with `skipped_reason="no-evidence"`. */ -import type { LlmClient } from "../llm/types.js"; +import type { LlmClient, LlmMessage } from "../llm/types.js"; import { detectModelRefusal } from "../llm/refusal.js"; import { detectDominantLanguage, @@ -174,6 +174,78 @@ export async function crystallizeDraft( }); return { ok: false, skippedReason: "llm-refusal", modelRefusal }; } + + // ── retry with correction context if raw output is available ── + if (rawPreview) { + log.warn("skill.crystallize.retry", { + policyId: input.policy.id, + error: message, + }); + const correctionMessages: LlmMessage[] = [ + { role: "system", content: SKILL_CRYSTALLIZE_PROMPT.system }, + { role: "system", content: languageSteeringLine(evidenceLang) }, + { role: "user", content: userPayload }, + { role: "assistant", content: rawPreview }, + { + role: "user", + content: `The previous attempt produced the following output: + +${rawPreview} + +The error was: ${message}. Please correct this and generate a valid JSON skill definition. Ensure the output is valid JSON and follows the required schema.`, + }, + ]; + try { + const rsp = await llm.completeJson>( + correctionMessages, + { + op: "skill.crystallize", + phase: "skill", + episodeId: input.episodeId, + schemaHint: "skill-crystallize.v2", + }, + ); + const retryRawRefusal = detectModelRefusal(rsp.raw); + if (retryRawRefusal) { + const modelRefusal = { + provider: rsp.provider, + model: rsp.model, + servedBy: rsp.servedBy, + ...retryRawRefusal, + }; + log.error("skill.crystallize.retry_model_refusal", { + policyId: input.policy.id, + ...modelRefusal, + }); + return { ok: false, skippedReason: "llm-refusal", modelRefusal }; + } + const draft = normaliseDraft(rsp.value, input); + const draftRefusal = detectModelRefusal(draft); + if (draftRefusal) { + const modelRefusal = { + provider: rsp.provider, + model: rsp.model, + servedBy: rsp.servedBy, + ...draftRefusal, + }; + log.error("skill.crystallize.retry_model_refusal", { + policyId: input.policy.id, + ...modelRefusal, + }); + return { ok: false, skippedReason: "llm-refusal", modelRefusal }; + } + if (deps.validate) deps.validate(draft); + return { ok: true, draft }; + } catch (retryErr) { + const retryMsg = retryErr instanceof Error ? retryErr.message : String(retryErr); + log.error("skill.crystallize.retry_failed", { + policyId: input.policy.id, + error: retryMsg, + }); + return { ok: false, skippedReason: `llm-failed: ${retryMsg}` }; + } + } + log.error("skill.crystallize.failed", { policyId: input.policy.id, error: message }); return { ok: false, skippedReason: `llm-failed: ${message}` }; } From c3eef512d094303b2413caaaf728994ec3691324 Mon Sep 17 00:00:00 2001 From: 34262315716 <34262315716@users.noreply.github.com> Date: Mon, 6 Jul 2026 20:36:28 +0800 Subject: [PATCH 2/3] chore: add retry_succeeded log so successful rescues are visible When the correction retry rescues a previously-failed crystallization, log a warning with policyId and the original error so operators can distinguish first-try successes from retry-rescued ones. --- apps/memos-local-plugin/core/skill/crystallize.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/memos-local-plugin/core/skill/crystallize.ts b/apps/memos-local-plugin/core/skill/crystallize.ts index 99516f736..54c1f6c6a 100644 --- a/apps/memos-local-plugin/core/skill/crystallize.ts +++ b/apps/memos-local-plugin/core/skill/crystallize.ts @@ -235,6 +235,10 @@ The error was: ${message}. Please correct this and generate a valid JSON skill d return { ok: false, skippedReason: "llm-refusal", modelRefusal }; } if (deps.validate) deps.validate(draft); + log.warn("skill.crystallize.retry_succeeded", { + policyId: input.policy.id, + error: message, + }); return { ok: true, draft }; } catch (retryErr) { const retryMsg = retryErr instanceof Error ? retryErr.message : String(retryErr); From c624154adb8eb217fe52ac50747a20c14a936bb1 Mon Sep 17 00:00:00 2001 From: 34262315716 <34262315716@users.noreply.github.com> Date: Mon, 6 Jul 2026 20:56:52 +0800 Subject: [PATCH 3/3] fix: allow retry when raw response is empty string (not just null) The condition treats empty string as falsy, so when the LLM returns blank output the correction retry was never triggered. Changed to so empty responses also get a retry. --- apps/memos-local-plugin/core/skill/crystallize.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/memos-local-plugin/core/skill/crystallize.ts b/apps/memos-local-plugin/core/skill/crystallize.ts index 54c1f6c6a..c45c5453d 100644 --- a/apps/memos-local-plugin/core/skill/crystallize.ts +++ b/apps/memos-local-plugin/core/skill/crystallize.ts @@ -176,7 +176,7 @@ export async function crystallizeDraft( } // ── retry with correction context if raw output is available ── - if (rawPreview) { + if (rawPreview !== null) { log.warn("skill.crystallize.retry", { policyId: input.policy.id, error: message,