import { t as createSubsystemLogger } from "./subsystem-qDUB67UD.js";
import { p as resolveDefaultModelForAgent } from "./model-selection-DCVjDyLz.js";
import "./defaults-COZq2n5p.js";
import { n as prepareSimpleCompletionModel } from "./simple-completion-runtime-BkSLbXlj.js";
import { a as pickPromptVariant } from "./preference-learner-DAFxYElO.js";
import path from "node:path";
import os from "node:os";
import fs from "node:fs/promises";
import { randomUUID } from "node:crypto";
import { complete } from "@earendil-works/pi-ai";
//#region src/cognitive/insight/content-similarity.ts
function extractTrigrams(text) {
	const trigrams = /* @__PURE__ */ new Set();
	const normalized = text.toLowerCase().replace(/\s+/g, " ").trim();
	for (let i = 0; i <= normalized.length - 3; i++) trigrams.add(normalized.slice(i, i + 3));
	return trigrams;
}
function computeTrigramSimilarity(a, b) {
	if (a.length < 3 || b.length < 3) return 0;
	const trigramsA = extractTrigrams(a);
	const trigramsB = extractTrigrams(b);
	if (trigramsA.size === 0 || trigramsB.size === 0) return 0;
	let overlap = 0;
	for (const t of trigramsA) if (trigramsB.has(t)) overlap++;
	return overlap / Math.max(trigramsA.size, trigramsB.size);
}
const CHINESE_STOP_CHARS = "的了是在和与但也都就把被让对给从到为以其之所等着过得地会有能可以要将于中上下里外时后前间个这那什么怎么哪几多少";
function extractChinesePhrases(text) {
	let spaced = text;
	for (const ch of CHINESE_STOP_CHARS) spaced = spaced.replaceAll(ch, " ");
	return spaced.split(/[\s,，。.？?！!；;：:、""''「」【】（）()[\]{}<>《》/\\—–-]+/).map((s) => s.trim()).filter((s) => s.length >= 2 && s.length <= 20);
}
function computeContentWordOverlap(a, b) {
	const phrasesA = extractChinesePhrases(a);
	const phrasesB = extractChinesePhrases(b);
	if (phrasesA.length === 0 || phrasesB.length === 0) return 0;
	const cjkFilter = (s) => [...s].filter((ch) => ch.charCodeAt(0) > 11903);
	const charsA = new Set(cjkFilter(phrasesA.join("")).map((c) => c.toLowerCase()));
	const charsB = new Set(cjkFilter(phrasesB.join("")).map((c) => c.toLowerCase()));
	if (charsA.size === 0 || charsB.size === 0) return 0;
	let intersection = 0;
	for (const ch of charsA) if (charsB.has(ch)) intersection++;
	const union = new Set([...charsA, ...charsB]).size;
	return union > 0 ? intersection / union : 0;
}
function extractContentThemes(contents) {
	if (contents.length === 0) return [];
	const freq = /* @__PURE__ */ new Map();
	for (const text of contents) for (const phrase of extractChinesePhrases(text)) freq.set(phrase, (freq.get(phrase) ?? 0) + 1);
	return [...freq.entries()].toSorted((a, b) => b[1] - a[1]).slice(0, 15).map(([phrase]) => phrase);
}
function isDuplicateBySemanticOverlap(newContent, recentContents, options) {
	const trigramThreshold = options?.trigramThreshold ?? .6;
	const contentWordThreshold = options?.contentWordThreshold ?? .15;
	for (const recent of recentContents) {
		if (computeTrigramSimilarity(newContent, recent) > trigramThreshold) return true;
		if (computeContentWordOverlap(newContent, recent) > contentWordThreshold) return true;
	}
	return false;
}
//#endregion
//#region src/cognitive/insight/interest-inference.ts
const log$1 = createSubsystemLogger("cognitive/interest-inference");
function resolveInferenceModel(config) {
	const explicit = config.cognitive?.insight?.inferenceModel ?? config.cognitive?.persona?.extractionModel;
	if (explicit) return explicit;
	const resolved = resolveDefaultModelForAgent({ cfg: config });
	return `${resolved.provider}/${resolved.model}`;
}
function parseSearchStrategyResponse(raw) {
	const cleaned = raw.replace(/^```(?:json)?\s*/m, "").replace(/\s*```$/m, "").trim();
	let parsed;
	try {
		parsed = JSON.parse(cleaned);
	} catch {
		return {
			ok: false,
			error: `Invalid JSON in inference response: ${cleaned.slice(0, 120)}`
		};
	}
	if (typeof parsed.inferredInterest !== "string" || typeof parsed.searchQuery !== "string" || typeof parsed.bridgeReasoning !== "string" || !Array.isArray(parsed.avoidTopics) || typeof parsed.estimatedSurprise !== "number") return {
		ok: false,
		error: "Missing required fields in inference response"
	};
	const estimatedSurprise = clamp01$1(parsed.estimatedSurprise);
	return {
		ok: true,
		strategy: {
			inferredInterest: parsed.inferredInterest,
			searchQuery: parsed.searchQuery,
			bridgeReasoning: parsed.bridgeReasoning,
			avoidTopics: parsed.avoidTopics.map(String),
			estimatedSurprise
		}
	};
}
function clamp01$1(value) {
	return Math.max(0, Math.min(1, value));
}
function buildInterestInferencePrompt(persona, input, mode = "surprise", fragments) {
	const domainEntries = Object.entries(persona.domains).toSorted(([, a], [, b]) => b.depth - a.depth);
	const knownKnowledge = domainEntries.length > 0 ? domainEntries.slice(0, 20).map(([name, d]) => {
		const insights = getFilteredInsights(d).slice(0, 3).join("; ");
		return `- ${name} (depth: ${d.depth})${insights ? ` — ${insights}` : ""}`;
	}).join("\n") : "(no domains established yet)";
	const expertDomains = persona.identity.expertDomains ?? [];
	const interestDomains = persona.identity.interestDomains ?? [];
	const curiosityDomains = persona.identity.curiosityDomains ?? [];
	const explicitInterests = [
		...expertDomains.map((d) => `[expert] ${d}`),
		...interestDomains.map((d) => `[interest] ${d}`),
		...curiosityDomains.map((d) => `[curiosity] ${d}`)
	];
	const explicitBlock = explicitInterests.length > 0 ? explicitInterests.join("\n") : "(not yet established)";
	const curiositySet = new Set(curiosityDomains);
	const knownDomainSet = new Set(Object.keys(persona.domains));
	const gaps = [...curiositySet].filter((d) => !knownDomainSet.has(d) || (persona.domains[d]?.depth ?? 0) < 2);
	const fragmentGaps = (fragments ?? []).filter((f) => f.kind === "knowledge_gap").slice(0, 4).map((f) => f.evidence);
	const activeQuestions = domainEntries.slice(0, 5).flatMap(([, d]) => d.activeQuestions ?? []).slice(0, 4);
	const allGaps = [
		...gaps,
		...fragmentGaps.filter((g) => !gaps.some((existing) => existing.toLowerCase() === g.toLowerCase())),
		...activeQuestions.filter((q) => !gaps.some((e) => e.toLowerCase() === q.toLowerCase()) && !fragmentGaps.some((f) => f.toLowerCase() === q.toLowerCase()))
	];
	const gapsBlock = allGaps.length > 0 ? allGaps.map((d) => `- ${d}`).join("\n") : "(no clear gaps identified)";
	const connectionsBlock = "(no domain connections yet)";
	const recentFocus = persona.recentFocus.slice(0, 5);
	const recentFocusBlock = recentFocus.length > 0 ? recentFocus.map((f) => `- ${f}`).join("\n") : "(none)";
	const topDiscussed = domainEntries.toSorted(([, a], [, b]) => b.recurrence - a.recurrence).slice(0, 3).map(([name]) => name);
	const isExtend = mode === "extend";
	const hasTargetDomains = input.targetDomains.length > 0;
	const targetDomainsBlock = hasTargetDomains ? `\n## TARGET DOMAIN FOR THIS INSIGHT\nThe insight pipeline has selected the following domain(s) as the focus area for this insight:\n${input.targetDomains.map((d) => `- ${d}`).join("\n")}\n\nThe searchQuery MUST be specifically about or related to these target domain(s).\n` : "";
	const taskInstruction = isExtend ? hasTargetDomains ? `Generate a focused search query to deepen understanding of "${input.targetDomains[0]}". Find a surprising angle, recent development, or lesser-known aspect within this specific domain.` : `Analyze this user's knowledge structure. Identify their MOST ACTIVE domain and generate a focused search query to deepen understanding of it. The query should target practical applications or recent developments in that domain.` : hasTargetDomains ? `The pipeline has selected "${input.targetDomains.join("\" or \"")}" as the focus area. Find a SURPRISINGLY ADJACENT angle — the searchQuery MUST include at least one recognizable keyword from "${input.targetDomains.join("\" or \"")}" to stay grounded in the user's knowledge. The surprise should come from the ANGLE, not from drifting to an unrelated topic. Something the user would find surprising and valuable, but hasn't explicitly explored.` : `Analyze this user's knowledge structure. Identify a LATENT interest — something they would find surprising and valuable, but that they haven't explicitly explored. The latent interest should bridge from what they know to something adjacent but unexpected.`;
	const currentYear = (/* @__PURE__ */ new Date()).getFullYear();
	return `You are an expert at identifying latent interests and knowledge gaps from a user's knowledge profile.

## USER'S KNOWN KNOWLEDGE
${knownKnowledge}

## EXPLICIT INTERESTS
${explicitBlock}

## KNOWLEDGE GAPS
${gapsBlock}

## DOMAIN CONNECTIONS
${connectionsBlock}

## RECENT FOCUS
${recentFocusBlock}
${targetDomainsBlock}
## TASK
${taskInstruction}

Constraints:
${isExtend ? `- The searchQuery must be 2-6 English keywords targeting practical applications or recent developments in the user's most active domain
- The estimatedSurprise should be moderate (0.3–0.6) since this is for extending known domains
- Prefer queries that surface actionable content: tools, case studies, benchmarks, or how-to guides
- Include the year ${currentYear} in the searchQuery to prioritize recent content` : `- The searchQuery must be 2-6 English keywords suitable for a web search API
- The estimatedSurprise must be between 0.6 and 1.0 (this is for surprise-mode insights only)
- The searchQuery MUST contain at least one keyword from the target domains to prevent drift
- Do NOT suggest topics the user is already an expert in
- Prefer cross-domain bridges that connect two areas the user knows about in an unexpected way
- Include the year ${currentYear} in the searchQuery to surface cutting-edge developments`}
- avoidTopics should contain the user's most-discussed domains to avoid re-treading: ${topDiscussed.join(", ")}
- Prefer topics with recent developments (${currentYear - 2}-${currentYear}). The searchQuery should target current trends, new tools, recent research, or emerging techniques — not general knowledge or introductory content.

Respond with ONLY a JSON object (no markdown, no code fences):
{
  "inferredInterest": "A concise description of the latent interest",
  "searchQuery": "2-6 English keywords for web search",
  "bridgeReasoning": "Why this connects to the user's latent interests and what they already know",
  "avoidTopics": ["domain1", "domain2", "domain3"],
  "estimatedSurprise": 0.8
}`;
}
async function inferSearchStrategy(persona, input, config, deps, mode = "surprise") {
	const modelRef = resolveInferenceModel(config);
	try {
		const prepared = await deps.prepareModel(config, modelRef);
		if ("error" in prepared) {
			log$1.warn("model preparation failed", { error: prepared.error });
			return {
				ok: false,
				error: `Model preparation failed: ${prepared.error}`
			};
		}
		const messages = [{
			role: "user",
			content: buildInterestInferencePrompt(persona, input, mode),
			timestamp: Date.now()
		}];
		const text = (await deps.complete(prepared.model, { messages }, {
			apiKey: prepared.auth.apiKey,
			maxTokens: 2e3,
			temperature: .9,
			signal: AbortSignal.timeout(3e4)
		})).content.filter((block) => block.type === "text").map((block) => block.text).join("").trim();
		if (!text) {
			log$1.warn("LLM returned empty inference response");
			return {
				ok: false,
				error: "LLM returned empty response"
			};
		}
		const parsed = parseSearchStrategyResponse(text);
		if (parsed.ok) log$1.info("inferred search strategy", {
			inferredInterest: parsed.strategy.inferredInterest,
			searchQuery: parsed.strategy.searchQuery,
			estimatedSurprise: parsed.strategy.estimatedSurprise
		});
		else log$1.warn("failed to parse inference response", {
			error: parsed.error,
			raw: text.slice(0, 200)
		});
		return parsed;
	} catch (err) {
		const isTimeout = err instanceof DOMException && err.name === "TimeoutError";
		log$1.warn(`interest inference ${isTimeout ? "timed out" : "failed"}`, { error: String(err) });
		return {
			ok: false,
			error: `Interest inference failed: ${String(err)}`
		};
	}
}
//#endregion
//#region src/cognitive/insight/llm-engine.ts
const log = createSubsystemLogger("cognitive/insight-llm");
const EXCLUDED_INSIGHT_CATEGORIES = new Set(["tool_config", "contextual_fact"]);
function getFilteredInsights(domain, exclude = EXCLUDED_INSIGHT_CATEGORIES) {
	if (domain.insights && domain.insights.length > 0) return domain.insights.filter((i) => !exclude.has(i.category)).toSorted((a, b) => b.confidence * b.evidenceCount - a.confidence * a.evidenceCount).map((i) => i.text);
	return domain.keyInsights;
}
const SEARCH_PROVIDER_DOMAINS = new Set([
	"exa.ai",
	"tavily.com",
	"search.brave.com",
	"api.exa.ai",
	"api.tavily.com"
]);
function isSearchProviderUrl(url) {
	try {
		const host = new URL(url).hostname;
		return SEARCH_PROVIDER_DOMAINS.has(host) || [...SEARCH_PROVIDER_DOMAINS].some((d) => host.endsWith(`.${d}`) || host === d);
	} catch {
		return false;
	}
}
const FRAGMENT_KINDS_FOR_PROMPT = new Set([
	"knowledge_gap",
	"assumption",
	"implicit_priority"
]);
function classifyRhetoricalMove(text) {
	const t = text.trim();
	if (/(是否|难道|有没有|会不会)/.test(t.slice(0, 20))) return "提问式";
	if (/(其实|但.*实际上|表面上.*实际上|看似.*实则)/.test(t.slice(0, 30))) return "悖论式";
	if (/(建议|推荐|试试|用.*做|直接用)/.test(t.slice(0, 30))) return "推荐式";
	if (/(就像|好比|类似于|跟.*一样|本质上.*就是)/.test(t.slice(0, 30))) return "类比式";
	if (/^(你|你的)/.test(t)) return "观察式";
	return "事实开头";
}
function buildBannedOpeningsSection(recentInsightContents) {
	if (recentInsightContents.length === 0) return "";
	const charBans = recentInsightContents.slice(-5).map((c) => c.trim().slice(0, 8)).filter((o) => o.length >= 4).map((o) => `不要以"${o}"开头`).join("；");
	const moves = [...new Set(recentInsightContents.slice(-5).map(classifyRhetoricalMove))];
	return [charBans, moves.length > 0 ? `不要使用以下修辞手法作为开头：${moves.join("、")}` : ""].filter(Boolean).join("\n");
}
function buildFragmentSection(fragments) {
	const relevant = fragments.filter((f) => FRAGMENT_KINDS_FOR_PROMPT.has(f.kind));
	if (relevant.length === 0) return "";
	return [...relevant].toSorted((a, b) => b.strength - a.strength).slice(0, 6).map((f) => `- [${f.kind}] ${f.evidence}`).join("\n");
}
function buildVoiceSection(persona) {
	const style = persona.identity?.communicationStyle;
	const name = persona.identity?.displayName ?? "the user";
	const parts = [];
	parts.push(`You're writing to ${name}, a person you know well. This is a proactive message — like suddenly remembering something fascinating to tell a friend.`);
	if (style) {
		if (style.formality === "casual") parts.push("Tone: casual, like chatting with a close friend. Use 你 not 您.");
		else if (style.formality === "formal") parts.push("Tone: professional but warm. You can use 您 but keep it conversational.");
		else parts.push("Tone: natural and conversational. Match whatever feels right for the content.");
		if (style.technicalLevel === "expert") parts.push("Assume deep technical literacy. Use technical terms freely without explanation.");
		else if (style.technicalLevel === "beginner") parts.push("Explain technical concepts briefly when they appear. Avoid jargon.");
		if (style.verbosity === "concise") parts.push("Be brief: 1-2 sentences maximum. Every word earns its place.");
		else if (style.verbosity === "detailed") parts.push("You can use 2-3 sentences. Give enough context to be self-contained.");
	}
	return parts.join("\n");
}
const DIVERSE_FEW_SHOT_SETS = [
	{
		name: "数据先行 (Data-first)",
		examples: [{
			context: "User builds TypeScript + Rust stack. Node 22 ESM support is relevant.",
			chinese: "Node 22 的 require(ESM) 原生支持已经 stable 了——这意味着你 KaijiBot 里那些 dynamic import 可以直接换成 require，冷启动能省 15-30%。对你的 Rust sidecar + Node gateway 架构，冷启动延迟一直是痛点。",
			english: "Node 22's require(ESM) native support is now stable — meaning those dynamic imports in KaijiBot can switch to require directly, saving 15-30% on cold starts. For your Rust sidecar + Node gateway architecture, cold-start latency has always been a pain point."
		}, {
			context: "User tracks pnpm and monorepo tooling. pnpm 9.7 catalog feature.",
			chinese: "pnpm 9.7 的 catalog: 协议让你在 monorepo 里统一管理依赖版本——不用再每个 package.json 手动同步。你 extensions/ 下 62 个插件，光 react 版本不一致就出过两次构建错误。",
			english: "pnpm 9.7's catalog: protocol lets you unify dependency versions across a monorepo — no more manually syncing each package.json. With 62 plugins under extensions/, mismatched React versions alone caused two build failures."
		}]
	},
	{
		name: "直接推荐 (Direct recommendation)",
		examples: [{
			context: "User has complex cognitive pipeline with many async flows.",
			chinese: "用 Effect-TS 的 Layer 做你 cognitive pipeline 的依赖注入，替代手写的 createDefaultDeps 模式。因为你的 insight pipeline 有 5 层嵌套异步调用，Effect 的 fiber 调度能让你给每层加独立超时和重试，不用改调用结构。",
			english: "Use Effect-TS Layer for DI in your cognitive pipeline instead of the hand-rolled createDefaultDeps pattern. Because your insight pipeline has 5 levels of nested async calls, Effect's fiber scheduling gives you independent timeout+retry per layer without restructuring call sites."
		}, {
			context: "User does LLM-driven extraction with structured output.",
			chinese: "把 structured output 的 schema 校验从 zod passthrough 换成 strict mode——你现在 llm-extractor.ts 里有 6 个 z.object 用了 .passthrough()，LLM 返回多余字段时不会报错但下游取到 undefined。Strict mode 会在 extraction 阶段就拦住，省掉下游 debug 时间。",
			english: "Switch structured output schema validation from zod passthrough to strict mode — you have 6 z.object calls using .passthrough() in llm-extractor.ts. When the LLM returns extra fields, no error fires but downstream reads undefined. Strict mode catches this at extraction time, saving downstream debugging."
		}]
	},
	{
		name: "行为观察 (Behavioral observation)",
		examples: [{
			context: "User consistently extracts shared patterns into plugin SDK boundaries.",
			chinese: "你每次发现两个扩展做同样的事，第一反应不是合并代码，而是往 plugin-sdk 里加一个抽象层——extensions/AGENTS.md 那条 'no relative imports escaping package root' 规则就是这么来的。这比大多数人处理依赖的方式更可持续，但也意味着你的 SDK surface area 在持续膨胀。",
			english: "Every time you spot two extensions doing the same thing, your first move isn't merging code — it's adding an abstraction layer to plugin-sdk. The 'no relative imports escaping package root' rule in extensions/AGENTS.md came from exactly this. More sustainable than how most people handle deps, but it also means your SDK surface area keeps expanding."
		}, {
			context: "User makes architecture decisions by building competing prototypes.",
			chinese: "你选技术方案的方式不是对比文档，而是直接写两个最小原型然后看哪个跑起来更顺——FragmentStore 和 CorrectionStore 都走了这条路。这说明你对代码体感的信任超过对抽象推理的信任，适合你这种写 Rust 的人。",
			english: "You pick tech approaches not by comparing docs but by writing two minimal prototypes and seeing which feels better — FragmentStore and CorrectionStore both went through this path. It means you trust code feel over abstract reasoning, which fits someone who writes Rust."
		}]
	},
	{
		name: "悖论消解 (Paradox resolution)",
		examples: [{
			context: "User says they want minimal dependencies but keeps adding abstractions.",
			chinese: "你说要最小化依赖，但 plugin-sdk 的 export 列表已经 41 个了。这不是矛盾——你真正排斥的不是依赖本身，是黑箱依赖。你宁愿自己写 200 行 well-typed wrapper，也不愿引入一个 50KB 的第三方包。这是一个可预测的标准：只要你能看到源码、能改，就不算'外部依赖'。",
			english: "You say you want minimal dependencies, but plugin-sdk's export list is already 41 items. Not a contradiction — what you actually resist isn't dependencies themselves but black-box ones. You'd rather write a 200-line well-typed wrapper than pull in a 50KB third-party package. Predictable standard: if you can see the source and modify it, it doesn't count as an 'external dependency'."
		}, {
			context: "User values fast iteration but invests heavily in cognitive infrastructure.",
			chinese: "你追求快速迭代，却在认知层写了 10+ 模块、3 套存储引擎、还有个 Thompson Sampling 偏好系统。表面看是过度工程——其实你是在解决一个具体问题：你怎么知道主动推给用户的消息不会让他们烦？答案是需要量化模型。所以不是'慢下来做基础设施'，是'没有这个基础设施，核心功能根本不敢上线'。",
			english: "You chase fast iteration, yet built 10+ cognitive modules, 3 storage engines, and a Thompson Sampling preference system. Looks like over-engineering — but you're solving a concrete problem: how do you know proactive messages won't annoy users? The answer requires a quantitative model. So it's not 'slow down to build infra', it's 'without this infra, the core feature can't ship at all'."
		}]
	}
];
const DIVERSITY_INSTRUCTION = `These examples demonstrate the expected QUALITY LEVEL and DEPTH of observation. Do NOT copy their structure, sentence pattern, or opening style. Each insight must be uniquely shaped by the specific user data and fragments you see. Every insight should feel like it could ONLY be about THIS specific user. 禁止使用"你做X时用的正是Y哲学概念"或"X本质上就是Y"这类类比框架作为主要结构。当哲学确实是最佳角度时直接说哲学内容，不要用"你用的正是"句式包装。`;
const CONTRASTIVE_INSTRUCTION = `CONTRASTIVE FRAMEWORK — your insight MUST be genuinely NEW relative to past insights:
- COUNTER-EXAMPLE: If a past insight said "X is good", find a case where X fails or the opposite holds.
- INVERSE FRAMING: If a past insight opened with a fact, open with a question/stakes/paradox instead.
- ORTHOGONAL OBSERVATION: If past insights covered domain A∩B, find a completely different angle (historical, ethical, practical, engineering) on the same intersection.
- NOVELTY TEST: Before finalizing, check: "Could this insight be mistaken for a paraphrase of any past insight?" If yes, rewrite.`;
/**
* Build the default deps that hit real infrastructure.
* Import and call this in production code.
*/
function createDefaultInsightDeps() {
	return {
		complete,
		prepareModel: async (cfg, modelRef) => {
			const extractionModel = cfg.cognitive?.persona?.extractionModel;
			const explicit = modelRef ?? extractionModel;
			if (explicit) {
				const [provider, ...modelParts] = explicit.split("/");
				return prepareSimpleCompletionModel({
					cfg,
					provider,
					modelId: modelParts.join("/") || "glm-5-turbo"
				});
			}
			const resolved = resolveDefaultModelForAgent({ cfg });
			return prepareSimpleCompletionModel({
				cfg,
				provider: resolved.provider,
				modelId: resolved.model
			});
		},
		inferenceDeps: {
			complete,
			prepareModel: async (cfg, modelRef) => {
				const extractionModel = cfg.cognitive?.persona?.extractionModel;
				const explicit = modelRef ?? extractionModel;
				if (explicit) {
					const [provider, ...modelParts] = explicit.split("/");
					return prepareSimpleCompletionModel({
						cfg,
						provider,
						modelId: modelParts.join("/") || "glm-5-turbo"
					});
				}
				const resolved = resolveDefaultModelForAgent({ cfg });
				return prepareSimpleCompletionModel({
					cfg,
					provider: resolved.provider,
					modelId: resolved.model
				});
			}
		}
	};
}
/**
* LLM-based insight generation with template fallback.
*
* Sends persona + domain knowledge to the LLM and asks it to produce
* personalised insight candidates.  Falls back to the deterministic
* template engine whenever the LLM call fails, times out, or returns
* unparseable output.
*
* This function **never throws**.
*/
async function generateInsightCandidatesLLM(persona, input, config, deps, options) {
	const maxCandidates = options?.maxCandidates ?? 3;
	const mode = input.mode ?? "extend";
	if (mode === "pattern") {
		const { prompt, variant } = buildPatternInsightPrompt(persona, input, input.recentInsightContents);
		try {
			const modelRef = options?.modelRef ?? config.cognitive?.persona?.extractionModel;
			const prepared = await deps.prepareModel(config, modelRef);
			if ("error" in prepared) {
				log.warn(`LLM model preparation failed: ${prepared.error}, skipping insight`);
				return [];
			}
			const timeoutMs = options?.timeout ?? 2e4;
			const systemPrompt = options?.systemContext || void 0;
			const messages = [];
			messages.push({
				role: "user",
				content: prompt,
				timestamp: Date.now()
			});
			const text = (await deps.complete(prepared.model, {
				messages,
				systemPrompt
			}, {
				apiKey: prepared.auth.apiKey,
				maxTokens: options?.maxTokens ?? 2e3,
				temperature: .85,
				signal: AbortSignal.timeout(timeoutMs)
			})).content.filter((block) => block.type === "text").map((block) => block.text).join("").trim();
			if (!text) {
				log.warn("LLM returned empty response for pattern mode, skipping insight");
				return [];
			}
			const candidates = parseLLMInsights(text, maxCandidates);
			if (candidates.length === 0) {
				log.warn("LLM response could not be parsed as insights (pattern mode)", { raw: text.slice(0, 300) });
				return [];
			}
			log.info(`Pattern-mode LLM generated ${candidates.length} insight candidate(s)`);
			const recentContents = input.recentInsightContents;
			const filtered = recentContents.length > 0 ? candidates.filter((c) => !isDuplicateBySemanticOverlap(c.content, recentContents, {
				trigramThreshold: .85,
				contentWordThreshold: .5
			})) : candidates;
			if (filtered.length < candidates.length) log.info("pattern-mode trigram dedup filtered candidates", {
				before: candidates.length,
				after: filtered.length
			});
			return filtered.map((c) => {
				const inputDomains = input.targetDomains;
				const llmDomains = c.targetDomains;
				if (!llmDomains.some((d) => inputDomains.some((id) => id.toLowerCase() === d.toLowerCase())) && inputDomains.length > 0) {
					log.info("force-aligned pattern-mode LLM output domains to input targetDomains", {
						llmDomains,
						inputDomains
					});
					c.targetDomains = [...inputDomains];
				}
				c.promptVariant = variant;
				return c;
			});
		} catch (err) {
			const isTimeout = err instanceof DOMException && err.name === "TimeoutError";
			log.warn(`Pattern-mode LLM insight generation ${isTimeout ? "timed out" : "failed"}: ${String(err)}, skipping insight`);
			return [];
		}
	}
	let webResults = [];
	let searchStrategy;
	let queryUsed;
	if (mode === "surprise" && deps.inferenceDeps) {
		const inferenceResult = await inferSearchStrategy(persona, input, config, deps.inferenceDeps);
		if (inferenceResult.ok) {
			searchStrategy = inferenceResult.strategy;
			if (deps.webSearch && searchStrategy.searchQuery) {
				queryUsed = searchStrategy.searchQuery;
				try {
					webResults = (await cachedWebSearch(deps.webSearch, searchStrategy.searchQuery)).filter((r) => !isSearchProviderUrl(r.url));
					log.info("surprise-mode web search completed", {
						query: searchStrategy.searchQuery,
						resultCount: webResults.length
					});
				} catch (err) {
					log.warn("surprise-mode web search failed", {
						query: searchStrategy.searchQuery,
						error: String(err)
					});
				}
			}
		} else {
			log.info("inference failed, falling back to extend mode", { error: inferenceResult.error });
			return generateExtendMode(persona, input, config, deps, options, maxCandidates);
		}
	} else if (deps.webSearch) {
		let query;
		if (deps.inferenceDeps) try {
			const inferenceResult = await inferSearchStrategy(persona, input, config, deps.inferenceDeps, "extend");
			if (inferenceResult.ok && inferenceResult.strategy.searchQuery) {
				query = inferenceResult.strategy.searchQuery;
				log.info("extend-mode LLM query generated", { query });
			}
		} catch (err) {
			log.warn("extend-mode inference failed, falling back to rule-based query", { error: String(err) });
		}
		if (!query) query = buildSearchQuery(input) || void 0;
		queryUsed = query;
		if (query) try {
			webResults = (await cachedWebSearch(deps.webSearch, query)).filter((r) => !isSearchProviderUrl(r.url));
			log.info("web search completed", {
				query,
				resultCount: webResults.length
			});
		} catch (err) {
			log.warn("web search failed, proceeding without web results", {
				query,
				error: String(err)
			});
			webResults = [];
		}
		else log.info("web search skipped: empty query");
	} else log.info("web search skipped: no webSearch dep provided");
	const outputLanguage = config.cognitive?.insight?.outputLanguage ?? detectOutputLanguage(persona);
	let webSnippetByDomain;
	if (webResults.length > 0) {
		const keywordMap = buildDomainKeywordMap(persona.domains);
		for (const td of input.targetDomains) if (!keywordMap.has(td)) {
			const keywords = /* @__PURE__ */ new Set();
			keywords.add(td.toLowerCase());
			for (const part of td.split(/[/+]/)) {
				const trimmed = part.trim().toLowerCase();
				if (trimmed.length >= 2) keywords.add(trimmed);
			}
			keywordMap.set(td, keywords);
		}
		webSnippetByDomain = matchWebResultsToDomains(webResults, keywordMap);
	}
	const { prompt, variant } = mode === "surprise" && searchStrategy ? buildSurpriseInsightPrompt(persona, input, webResults, input.recentInsightContents, searchStrategy, outputLanguage, webSnippetByDomain) : buildInsightPrompt(persona, input, webResults, input.recentInsightContents, webSnippetByDomain);
	try {
		const modelRef = options?.modelRef ?? config.cognitive?.persona?.extractionModel;
		const prepared = await deps.prepareModel(config, modelRef);
		if ("error" in prepared) {
			log.warn(`LLM model preparation failed: ${prepared.error}, skipping insight`);
			return [];
		}
		const timeoutMs = options?.timeout ?? 2e4;
		const systemPrompt = options?.systemContext || void 0;
		const messages = [];
		messages.push({
			role: "user",
			content: prompt,
			timestamp: Date.now()
		});
		const text = (await deps.complete(prepared.model, {
			messages,
			systemPrompt
		}, {
			apiKey: prepared.auth.apiKey,
			maxTokens: options?.maxTokens ?? 2e3,
			temperature: .85,
			signal: AbortSignal.timeout(timeoutMs)
		})).content.filter((block) => block.type === "text").map((block) => block.text).join("").trim();
		if (!text) {
			log.warn("LLM returned empty response, skipping insight");
			return [];
		}
		const candidates = parseLLMInsights(text, maxCandidates);
		if (candidates.length === 0) {
			log.warn("LLM response could not be parsed as insights", { raw: text.slice(0, 300) });
			return [];
		}
		log.info(`LLM generated ${candidates.length} insight candidate(s)`);
		const recentContents = input.recentInsightContents;
		const filtered = recentContents.length > 0 ? candidates.filter((c) => !isDuplicateBySemanticOverlap(c.content, recentContents, {
			trigramThreshold: .85,
			contentWordThreshold: .5
		})) : candidates;
		if (filtered.length < candidates.length) log.info("trigram dedup filtered candidates", {
			before: candidates.length,
			after: filtered.length
		});
		return filtered.map((c) => {
			const inputDomains = input.targetDomains;
			const llmDomains = c.targetDomains;
			if (!llmDomains.some((d) => inputDomains.some((id) => id.toLowerCase() === d.toLowerCase())) && inputDomains.length > 0) {
				log.info("force-aligned LLM output domains to input targetDomains", {
					llmDomains,
					inputDomains
				});
				c.targetDomains = [...inputDomains];
			}
			const enriched = enrichWithWebSources(c, webResults);
			if (queryUsed) enriched.searchQueryUsed = queryUsed;
			enriched.promptVariant = variant;
			return enriched;
		});
	} catch (err) {
		const isTimeout = err instanceof DOMException && err.name === "TimeoutError";
		log.warn(`LLM insight generation ${isTimeout ? "timed out" : "failed"}: ${String(err)}, skipping insight`);
		return [];
	}
}
async function generateExtendMode(persona, input, config, deps, options, maxCandidates) {
	return generateInsightCandidatesLLM(persona, {
		...input,
		mode: "extend"
	}, config, deps, {
		...options,
		maxCandidates
	});
}
function detectOutputLanguage(persona) {
	const lang = persona.identity?.primaryLanguage ?? persona.identity?.communicationStyle?.preferredLanguage;
	if (lang === "en") return "en";
	if (lang === "mixed") return "zh";
	return "zh";
}
/**
* Strip conversational noise from a raw user utterance and keep only
* the noun-phrases / technical terms that are useful as search keywords.
*
* Removes:
*  - Feishu user-ID prefixes (`ou_xxx:`, `9cc3e...:`)
*  - Common Chinese conversational fillers / question wrappers
*  - Leading interrogatives (你能不能, 我能怎么, 为什么 etc.)
*  - Stray punctuation
*/
function extractKeyTerms(text) {
	let cleaned = text.replace(/\bou_[0-9a-f]+\s*:?\s*/g, "").replace(/\b[0-9a-f]{16,}\s*:?\s*/g, "").replace(/^(?:需要我|你能|我能不能|我能怎么|你为什么|是不是|你好[，,]?|请问|能不能|为什么)/, "").replace(/[？?，,。.！!]+$/g, "").replace(/(?:才能|的话|到底|这个|那个|一下|帮我|帮我去)/g, " ").trim();
	if (!cleaned) return [];
	return cleaned.split(/[，,？?；;、—–]+|(?:的?时候|之前|之后|还是)/).flatMap((s) => {
		const trimmed = s.trim();
		if (!trimmed) return [];
		if (trimmed.length <= 30 && trimmed.length >= 2) return [trimmed];
		if (trimmed.length > 30) return trimmed.split(/\s+/).filter((w) => w.length >= 2 && w.length <= 30);
		return [];
	});
}
const SUFFIXES = [
	" 最新进展",
	" 实践案例",
	" 最佳实践",
	" 技术趋势",
	" 新方向"
];
const SEARCH_CACHE_TTL_MS = 7200 * 1e3;
const MAX_CACHE_ENTRIES = 100;
const searchCache = /* @__PURE__ */ new Map();
function cachedWebSearch(webSearch, query) {
	const now = Date.now();
	const cached = searchCache.get(query);
	if (cached && now - cached.fetchedAt < SEARCH_CACHE_TTL_MS) {
		log.info("web search cache hit", { query });
		return Promise.resolve(cached.results);
	}
	if (searchCache.size >= MAX_CACHE_ENTRIES) {
		const staleKeys = [];
		for (const [k, v] of searchCache) if (now - v.fetchedAt >= SEARCH_CACHE_TTL_MS) staleKeys.push(k);
		if (staleKeys.length > 0) for (const k of staleKeys) searchCache.delete(k);
		else {
			const firstKey = searchCache.keys().next().value;
			if (firstKey !== void 0) searchCache.delete(firstKey);
		}
	}
	return webSearch(query).then((results) => {
		searchCache.set(query, {
			results,
			fetchedAt: now
		});
		return results;
	});
}
function buildSearchQuery(input) {
	const parts = [];
	const seen = /* @__PURE__ */ new Set();
	const historyTerms = /* @__PURE__ */ new Set();
	const history = input.recentQueryHistory ?? [];
	for (const query of history.slice(-3)) {
		for (const term of extractKeyTerms(query)) historyTerms.add(term.toLowerCase());
		for (const word of query.split(/\s+/)) if (word.length >= 2) historyTerms.add(word.toLowerCase());
	}
	for (const domain of input.targetDomains) {
		const terms = domain.split(/[/+\-\s]+/).filter((p) => p.length > 0);
		if (terms.length > 0 && terms.every((t) => historyTerms.has(t.toLowerCase())) && input.targetDomains.length > 1) continue;
		for (const term of terms) {
			const lower = term.toLowerCase();
			if (!seen.has(lower)) {
				parts.push(term);
				seen.add(lower);
			}
		}
		if (parts.length >= 3) break;
	}
	if (parts.length < 4 && input.recentFocus.length > 0) for (let fi = 0; fi < input.recentFocus.length && parts.length < 4; fi++) {
		const focusTerms = extractKeyTerms(input.recentFocus[fi]);
		for (const term of focusTerms) {
			const lower = term.toLowerCase();
			if (!seen.has(lower)) {
				parts.push(term);
				seen.add(lower);
			}
			if (parts.length >= 4) break;
		}
	}
	if (parts.length === 0) return "";
	const suffixIndex = parts.length <= 2 ? history.length % SUFFIXES.length : -1;
	const suffix = suffixIndex >= 0 ? SUFFIXES[suffixIndex] : "";
	const currentYear = (/* @__PURE__ */ new Date()).getFullYear().toString();
	const baseQuery = parts.join(" ") + suffix;
	return (baseQuery.includes(currentYear) ? baseQuery : `${baseQuery} ${currentYear}`).slice(0, 120);
}
function enrichWithWebSources(candidate, webResults) {
	if (webResults.length === 0) return candidate;
	return {
		...candidate,
		sources: webResults.map((r) => ({
			url: r.url,
			title: r.title,
			credibility: .5
		}))
	};
}
function buildSurpriseInsightPrompt(persona, input, webResults = [], recentInsightContents = [], strategy, outputLanguage = "zh", webSnippetByDomain) {
	const resolvedWebSnippetByDomain = webSnippetByDomain ?? matchWebResultsToDomains(webResults, buildDomainKeywordMap(persona.domains));
	const anchorFacts = Object.entries(persona.domains).toSorted(([, a], [, b]) => b.lastMentioned - a.lastMentioned).flatMap(([name, d]) => getFilteredInsights(d).slice(0, 2).map((ki) => `${name}: ${ki}`)).slice(0, 6);
	const anchorBlock = anchorFacts.length > 0 ? anchorFacts.map((f, i) => `${i + 1}. ${f}`).join("\n") : "  (not yet established)";
	const externalFacts = buildExternalFactsEntries(resolvedWebSnippetByDomain);
	const externalFactsBlock = externalFacts.length > 0 ? externalFacts.map((f, i) => `${i + 1}. ${f}`).join("\n") : "";
	const userName = persona.identity?.displayName || "";
	const identityBlock = persona.identity ? [
		userName ? `Name: ${userName}` : "",
		persona.identity.expertDomains?.length ? `Expert in: ${persona.identity.expertDomains.join(", ")}` : "",
		persona.identity.interestDomains?.length ? `Interested in: ${persona.identity.interestDomains.join(", ")}` : ""
	].filter(Boolean).join("\n") : "";
	const pastInsightBlock = recentInsightContents.length > 0 ? recentInsightContents.slice(-5).map((c, i) => `${i + 1}. ${truncate(c, 120)}`).join("\n") : "";
	const bannedSection = buildBannedOpeningsSection(recentInsightContents);
	const langInstruction = outputLanguage === "en" ? "Output in English." : "用中文输出。";
	const fewShotIdx = input.feedbackProfile ? pickPromptVariant(input.feedbackProfile, DIVERSE_FEW_SHOT_SETS.map((_, i) => `fewShot:${i}`)) : Math.floor(Math.random() * DIVERSE_FEW_SHOT_SETS.length);
	const fewShotBlock = DIVERSE_FEW_SHOT_SETS[fewShotIdx].examples.map((e) => `Context: ${e.context}\n中文: ${e.chinese}\nEnglish: ${e.english}`).join("\n\n");
	return {
		prompt: `${buildVoiceSection(persona)}

EXAMPLES of ideal insights (match this quality, specificity, and tone):
${fewShotBlock}

${DIVERSITY_INSTRUCTION}

CRITICAL: Output in your own voice — the same personality the user knows from regular conversations. NOT a formal report, NOT a system notification.

You are the AI assistant speaking in your own voice and personality. You are proactively reaching out to share something genuinely SURPRISING — something the user hasn't encountered but would find fascinating.

${identityBlock ? `USER:\n${identityBlock}` : ""}

INFERRED LATENT INTEREST:
  Interest: ${strategy.inferredInterest}
  Bridge: ${strategy.bridgeReasoning}
  Why surprising: This area is adjacent to what the user knows but explores a direction they haven't considered.

SPECIFIC FACTS YOU KNOW ABOUT THIS USER:
${anchorBlock}
 ${externalFactsBlock ? `\nEXTERNAL_FACTS (fresh web findings):\n${externalFactsBlock}` : ""}

${pastInsightBlock ? `\nPAST INSIGHTS (your insight must be CONTRASTIVELY different — see CONTRASTIVE FRAMEWORK below):\n${pastInsightBlock}\n\n${CONTRASTIVE_INSTRUCTION}` : ""}
${recentInsightContents.length > 0 ? `\nRECENTLY USED CONTENT THEMES (DO NOT reuse these concepts even for different domains):\n${extractContentThemes(recentInsightContents).join("、")}` : ""}
${(input.recentInsightDomains?.length ?? 0) > 0 ? `\nRECENTLY COVERED DOMAIN COMBINATIONS (insight MUST explore NEW territory, NOT repeat these):\n${input.recentInsightDomains.slice(-5).map((domains, i) => `${i + 1}. ${domains.join(" + ")}`).join("\n")}` : ""}

TASK:
Share a specific, surprising insight about "${strategy.inferredInterest}". Bridge from what the user already knows (${strategy.avoidTopics.join(", ")}) to this new territory. The insight should feel like a genuine discovery, not a recommendation or tutorial.

Constraints:
- 1-3 sentences, ${langInstruction}
- Tone: like suddenly remembering something fascinating to tell a friend
- NO question marks, NO lists, NO numbering
- Forbidden phrases: "值得关注", "挺有意思", "不得不说", "你有没有想过", "最近在关注", "有趣的是", "值得注意的是"
- Start with a concrete fact, counter-intuitive observation, or specific case — never with "关于", "在...领域", "结合你", "作为"
- ${bannedSection}
- Content must be a specific judgment, observation, or discovery — not vague feelings
${webResults.length > 0 ? "- Weave external information naturally, do NOT say 'saw', 'read', 'reportedly'" : ""}

Good surprise insight traits (hit at least one):
- Frontier bridge: connects user's existing knowledge to a genuinely new development
- Unexpected connection: reveals a hidden link the user wouldn't have noticed
- Paradigm shift: challenges an assumption the user likely holds

Respond with ONLY a JSON array (no markdown, no code fences):
IMPORTANT: In the "content" field, escape any inner quotes as \\" or use Chinese curly quotes (""). Do NOT use unescaped ASCII quotes inside string values.
[
  {
    "content": "Your surprising insight in your own voice",
    "rationale": "Why this is surprising and relevant to this user SPECIFICALLY",
    "targetDomains": ["inferred-domain"],
    "sourceDomains": ["user-known-domain"],
    "relevanceScore": 0.8,
    "surpriseScore": 0.7
  }
]`,
		variant: {
			fewShotSet: fewShotIdx,
			frameIndex: 0
		}
	};
}
function pickRandom(arr) {
	return arr.length > 0 ? arr[Math.floor(Math.random() * arr.length)] : void 0;
}
/** Prompt framework variants — each anchors on specific persona data to avoid generic output. */
const PROMPT_FRAMES = [
	(topic, extra) => {
		const insight = pickRandom(extra.keyInsights);
		if (insight) return `你了解到用户对"${insight}"有独到理解。从这个具体的认知出发，说出一个被大多数人忽略的延伸方向或实际应用场景。不要解释这个认知本身，直接说延伸的部分。`;
		return `针对${topic}，你有一个具体的观察——不是泛泛的感受，而是能直接指导下一步行动的判断。直接说出来。`;
	},
	(topic, extra) => {
		if (extra.domains.length >= 2 && extra.keyInsights.length >= 2) return `用户同时在${topic}和${extra.domains[extra.domains.length - 1]}两个方向有积累。你看到了一条具体的关联线索——不是概念上的相似，而是实际的、可操作的交集。直接把这条线索说出来。`;
		return `在${topic}方向上，用户目前的理解里有一个盲区。你看到了，直接指出来，不要铺垫。`;
	},
	(topic, _extra) => {
		return `你刚注意到${topic}领域一个具体的变化或案例，直接关系到用户之前提到的关注点。简洁地说出来。`;
	},
	(topic, extra) => {
		const insight = pickRandom(extra.keyInsights);
		if (insight) return `基于"${insight}"这个认知，常见的做法里有一个效率或思路上的问题。你有一个更好的替代方案——说出来，说清楚为什么更好。`;
		return `关于${topic}，你有一个来自实践的具体经验，跟大多数人的做法不一样。分享这个经验。`;
	},
	(topic, extra) => {
		return `用户最近在看${extra.recentFocus.length > 0 ? pickRandom(extra.recentFocus) : topic}相关的东西。你恰好知道一个具体的工具、方法或资源能直接帮上忙。推荐它，说清楚为什么适合现在的阶段。`;
	},
	(topic, extra) => {
		const insight = pickRandom(extra.keyInsights);
		if (insight) return `关于"${insight}"，有一个反直觉的事实。你把它说出来，用事实本身说话，不要加"有趣的是"之类的评论。`;
		return `在${topic}领域，你发现了一条被低估的技术路径或思路。说出它是什么，以及为什么被低估。`;
	},
	(topic, extra) => {
		if (extra.recentFocus.length >= 1) return `${topic}和${extra.recentFocus[Math.min(extra.recentFocus.length - 1, 1)]}之间有一条暗线——不是表面的关联，而是具体的工程方案或技术选型上的共通之处。直接说出这条暗线是什么。`;
		return `你注意到${topic}领域有一个正在发生但还没被广泛讨论的变化。说出它是什么。`;
	},
	(topic, extra) => {
		if (extra.domains.length >= 2) return `把${extra.domains[extra.domains.length - 1]}里的一个成熟做法，迁移到${topic}的场景中。说出具体的迁移方案和预期效果。`;
		return `给${topic}方向一个具体的下一步建议——不是方向性的，而是可以直接执行的那种。`;
	}
];
function pickPromptFrame(topics, domainNames, keyInsights, recentFocus, userName, feedbackProfile) {
	const topic = topics.length > 0 ? topics[0] : "你的兴趣领域";
	const frameIdx = feedbackProfile ? pickPromptVariant(feedbackProfile, PROMPT_FRAMES.map((_, i) => `frame:${i}`)) : Math.floor(Math.random() * PROMPT_FRAMES.length);
	const frame = PROMPT_FRAMES[frameIdx];
	return {
		text: frame(topic, {
			domains: domainNames,
			keyInsights,
			recentFocus,
			userName
		}),
		frameIndex: frameIdx
	};
}
const STRUCTURE_SEEDS = [
	"这次用一个具体的事实或数据点开头，不要用观点开头。",
	"这次先说结论或判断，再说原因，不要反过来。",
	"这次直接给一个可执行的建议，不要做分析。",
	"这次说一个具体的案例或例子，不要抽象概括。",
	"这次用一个反直觉的陈述开头。",
	"这次提出一个具体的技术选择或方案，说明为什么选它。",
	"这次指出一个常见的误区或错误做法，然后给出正确的方式。",
	"这次说一个具体的、可以直接执行的方法或方案。"
];
function getTimeTag(lastMentioned) {
	const hoursAgo = (Date.now() - lastMentioned) / (3600 * 1e3);
	if (hoursAgo < 24) return "active-today";
	if (hoursAgo < 72) return "recent";
	if (hoursAgo < 168) return "this-week";
	return "inactive";
}
function truncate(s, maxLen) {
	return s.length > maxLen ? s.slice(0, maxLen) + "…" : s;
}
function buildExternalFactsEntries(webSnippetByDomain) {
	return [...webSnippetByDomain.entries()].flatMap(([domain, snippets]) => snippets.map((s) => `[${domain}] ${truncate(s, 120)}`)).slice(0, 6);
}
function buildDomainKeywordMap(domains) {
	const map = /* @__PURE__ */ new Map();
	for (const [name, domain] of Object.entries(domains)) {
		const keywords = /* @__PURE__ */ new Set();
		keywords.add(name.toLowerCase());
		for (const part of name.split(/[/+]/)) {
			const trimmed = part.trim().toLowerCase();
			if (trimmed.length >= 2) keywords.add(trimmed);
		}
		for (const insight of getFilteredInsights(domain).slice(0, 3)) {
			const lower = insight.toLowerCase();
			keywords.add(lower);
			for (const word of lower.split(/\s+/)) if (word.length >= 3) keywords.add(word);
		}
		map.set(name, keywords);
	}
	return map;
}
function extractBigrams(text) {
	const bigrams = /* @__PURE__ */ new Set();
	const normalized = text.toLowerCase().replace(/\s+/g, "");
	for (let i = 0; i < normalized.length - 1; i++) bigrams.add(normalized.slice(i, i + 2));
	return bigrams;
}
function matchWebResultsToDomains(webResults, keywordMap) {
	const result = /* @__PURE__ */ new Map();
	for (const r of webResults) {
		const titleLower = r.title.toLowerCase();
		const snippetLower = r.snippet.toLowerCase();
		for (const [domainName, keywords] of keywordMap) if ([...keywords].some((kw) => {
			if (titleLower.includes(kw) || snippetLower.includes(kw)) return true;
			if (kw.length >= 4) {
				const kwBigrams = extractBigrams(kw);
				const textBigrams = extractBigrams(titleLower + " " + snippetLower);
				return [...kwBigrams].filter((b) => textBigrams.has(b)).length / Math.max(kwBigrams.size, 1) > .7;
			}
			return false;
		})) {
			const list = result.get(domainName) ?? [];
			list.push(r.snippet);
			result.set(domainName, list);
		}
	}
	return result;
}
const PATTERN_PROMPT_FRAMES = [
	(_topic, _extra) => "You notice a recurring thinking pattern across the user's conversations — describe what it is, when it helps, and when it becomes a limitation.",
	(_topic, _extra) => "The user has an implicit priority or value ranking visible across their decisions — name it and explain what it costs them.",
	(_topic, _extra) => "There's a contradiction between what the user says they want and what they actually do — point it out directly.",
	(_topic, _extra) => "There's a perspective or approach the user never considers — name it and explain why they're missing it."
];
function buildPatternInsightPrompt(persona, input, recentInsightContents) {
	const fewShotIdx = input.feedbackProfile ? pickPromptVariant(input.feedbackProfile, DIVERSE_FEW_SHOT_SETS.map((_, i) => `fewShot:${i}`)) : Math.floor(Math.random() * DIVERSE_FEW_SHOT_SETS.length);
	const fewShotBlock = DIVERSE_FEW_SHOT_SETS[fewShotIdx].examples.map((e) => `Context: ${e.context}\n中文: ${e.chinese}\nEnglish: ${e.english}`).join("\n\n");
	const sortedFragments = [...input.fragments ?? []].toSorted((a, b) => b.strength - a.strength).slice(0, 8);
	const fragmentBlock = sortedFragments.length > 0 ? sortedFragments.map((f) => `[${f.kind}] ${f.structuralTag}: "${truncate(f.evidence, 120)}" (strength: ${f.strength.toFixed(2)}, domains: ${f.domains.join(", ")})`).join("\n") : "(no fragments collected yet)";
	const anchorFacts = Object.entries(persona.domains).toSorted(([, a], [, b]) => b.lastMentioned - a.lastMentioned).flatMap(([name, d]) => getFilteredInsights(d).slice(0, 2).map((ki) => `${name}: ${ki}`)).slice(0, 6);
	const anchorBlock = anchorFacts.length > 0 ? anchorFacts.map((f, i) => `${i + 1}. ${f}`).join("\n") : "  (not yet established)";
	const pastInsightBlock = recentInsightContents.length > 0 ? recentInsightContents.slice(-5).map((c, i) => `${i + 1}. ${truncate(c, 120)}`).join("\n") : "";
	const bannedSection = buildBannedOpeningsSection(recentInsightContents);
	const patternFrameIdx = input.feedbackProfile ? pickPromptVariant(input.feedbackProfile, PATTERN_PROMPT_FRAMES.map((_, i) => `pattern:${i}`)) : Math.floor(Math.random() * PATTERN_PROMPT_FRAMES.length);
	const frame = PATTERN_PROMPT_FRAMES[patternFrameIdx];
	const taskInstruction = frame(input.targetDomains.join(", "), {
		domains: input.targetDomains,
		keyInsights: anchorFacts,
		recentFocus: input.recentFocus,
		userName: persona.identity?.displayName ?? ""
	});
	return {
		prompt: `${buildVoiceSection(persona)}

EXAMPLES of ideal behavioral observations (match this quality, specificity, and depth):
${fewShotBlock}

${DIVERSITY_INSTRUCTION}

CRITICAL: Output in your own voice — the same personality the user knows from regular conversations. NOT a formal report, NOT a system notification, NOT a therapy session.

You are the AI assistant speaking in your own voice and personality. You are proactively sharing a behavioral observation — something you noticed about how this user thinks, decides, or acts across their conversations.

OBSERVED THINKING PATTERNS (from recent conversations):
${fragmentBlock}

SPECIFIC FACTS YOU KNOW ABOUT THIS USER:
${anchorBlock}
 ${pastInsightBlock ? `\nPAST INSIGHTS (your insight must be CONTRASTIVELY different — see CONTRASTIVE FRAMEWORK below):\n${pastInsightBlock}\n\n${CONTRASTIVE_INSTRUCTION}` : ""}
${recentInsightContents.length > 0 ? `\nRECENTLY USED CONTENT THEMES (DO NOT reuse these concepts):\n${extractContentThemes(recentInsightContents).join("、")}` : ""}

TASK:
${taskInstruction}

Constraints:
- 1-3 sentences, Chinese
- No question marks, no lists, no numbering
- Forbidden phrases: "值得关注", "挺有意思", "不得不说", "你有没有想过", "最近在关注", "有趣的是", "值得注意的是"
- Start with a concrete observation — never with "关于", "在...领域", "结合你", "作为"
- ${bannedSection}
- Do NOT mention "patterns", "blind spots", "cognitive biases", or use meta-analytical language. Speak as a friend sharing an observation, not as a therapist diagnosing.
- Content must reference AT LEAST ONE specific fragment from the OBSERVED THINKING PATTERNS section above
- Content must be a specific, honest observation — not vague encouragement or generic advice

Respond with ONLY a JSON array (no markdown, no code fences):
重要提示：在 "content" 字段中，请用 \\" 转义内部引号，或使用中文弯引号（""）。不要在字符串值中使用未转义的 ASCII 引号。
[
  {
    "content": "Your behavioral observation in your own voice, in Chinese",
    "rationale": "Which fragments and persona data led to this observation",
    "targetDomains": ["domain-from-fragments"],
    "sourceDomains": ["observed-pattern"],
    "relevanceScore": 0.8,
    "surpriseScore": 0.7
  }
]

Keep insights concise (1-3 sentences). Quality over quantity.`,
		variant: {
			fewShotSet: fewShotIdx,
			frameIndex: 0,
			patternFrame: patternFrameIdx
		}
	};
}
function buildInsightPrompt(persona, input, webResults = [], recentInsightContents = [], webSnippetByDomain) {
	let resolvedWebSnippetByDomain;
	if (webSnippetByDomain) resolvedWebSnippetByDomain = webSnippetByDomain;
	else {
		const keywordMap = buildDomainKeywordMap(persona.domains);
		for (const td of input.targetDomains) if (!keywordMap.has(td)) {
			const keywords = /* @__PURE__ */ new Set();
			keywords.add(td.toLowerCase());
			for (const part of td.split(/[/+]/)) {
				const trimmed = part.trim().toLowerCase();
				if (trimmed.length >= 2) keywords.add(trimmed);
			}
			keywordMap.set(td, keywords);
		}
		resolvedWebSnippetByDomain = matchWebResultsToDomains(webResults, keywordMap);
	}
	if (webResults.length > 0) {
		const matchedDomains = [...resolvedWebSnippetByDomain.keys()];
		const matchedUrls = /* @__PURE__ */ new Set();
		for (const result of webResults) for (const snippets of resolvedWebSnippetByDomain.values()) if (snippets.some((s) => s === result.snippet)) {
			matchedUrls.add(result.url);
			break;
		}
		const unmatched = webResults.length - matchedUrls.size;
		log.info("web search domain matching", {
			totalResults: webResults.length,
			matchedDomains,
			unmatchedSnippets: unmatched
		});
		if (matchedDomains.length === 0) log.warn("web search domain matching: no domains matched, LLM generation will proceed without domain-grounded evidence");
	}
	const sortedDomainEntries = Object.entries(persona.domains).toSorted(([, a], [, b]) => b.lastMentioned - a.lastMentioned);
	const userDomains = sortedDomainEntries.slice(0, 8).map(([name, d]) => {
		const parts = [`${name} [${getTimeTag(d.lastMentioned)}, depth: ${d.depth}]`];
		const filtered = getFilteredInsights(d);
		if (filtered.length > 0) parts.push(`known: ${filtered.slice(0, 3).join("; ")}`);
		return parts.join(" | ");
	}).join("\n");
	const anchorFacts = sortedDomainEntries.flatMap(([name, d]) => getFilteredInsights(d).slice(0, 2).map((ki) => `${name}: ${ki}`)).slice(0, 6);
	const anchorBlock = anchorFacts.length > 0 ? anchorFacts.map((f, i) => `${i + 1}. ${f}`).join("\n") : "  (not yet established)";
	const externalFacts = buildExternalFactsEntries(resolvedWebSnippetByDomain);
	const externalFactsBlock = externalFacts.length > 0 ? externalFacts.map((f, i) => `${i + 1}. ${f}`).join("\n") : "";
	const recentFocus = persona.recentFocus.slice(0, 5).join(", ");
	const recentInsightIds = input.recentInsightIds.slice(0, 5).join(", ");
	const userName = persona.identity?.displayName || "";
	const identityBlock = persona.identity ? [
		userName ? `Name: ${userName}` : "",
		persona.identity.coreTraits ? `Traits: ${Object.entries(persona.identity.coreTraits).filter(([, v]) => v.confidence >= .5).map(([k, v]) => `${k}: ${v.value}`).join(", ")}` : "",
		persona.identity.expertDomains?.length ? `Expert in: ${persona.identity.expertDomains.join(", ")}` : "",
		persona.identity.interestDomains?.length ? `Interested in: ${persona.identity.interestDomains.join(", ")}` : ""
	].filter(Boolean).join("\n") : "";
	const pastInsightBlock = recentInsightContents.length > 0 ? recentInsightContents.slice(-5).map((c, i) => `${i + 1}. ${truncate(c, 120)}`).join("\n") : "";
	const bannedSection = buildBannedOpeningsSection(recentInsightContents);
	const domainNames = sortedDomainEntries.map(([name]) => name);
	const flatKeyInsights = sortedDomainEntries.flatMap(([, d]) => getFilteredInsights(d).slice(0, 2));
	const { text: promptFrame, frameIndex } = pickPromptFrame(input.targetDomains, domainNames, flatKeyInsights, persona.recentFocus, userName, input.feedbackProfile);
	const structureSeedIdx = input.feedbackProfile ? pickPromptVariant(input.feedbackProfile, STRUCTURE_SEEDS.map((_, i) => `seed:${i}`)) : Math.floor(Math.random() * STRUCTURE_SEEDS.length);
	const structureSeed = STRUCTURE_SEEDS[structureSeedIdx];
	const fewShotIdx = input.feedbackProfile ? pickPromptVariant(input.feedbackProfile, DIVERSE_FEW_SHOT_SETS.map((_, i) => `fewShot:${i}`)) : Math.floor(Math.random() * DIVERSE_FEW_SHOT_SETS.length);
	const fewShotBlock = DIVERSE_FEW_SHOT_SETS[fewShotIdx].examples.map((e) => `Context: ${e.context}\n中文: ${e.chinese}\nEnglish: ${e.english}`).join("\n\n");
	const fragmentSection = buildFragmentSection(input.fragments ?? []);
	return {
		prompt: `${buildVoiceSection(persona)}

EXAMPLES of ideal insights (match this quality, specificity, and tone):
${fewShotBlock}

${DIVERSITY_INSTRUCTION}

CRITICAL: Output in your own voice — the same personality the user knows from regular conversations. NOT a formal report, NOT a system notification.

You are the AI assistant speaking in your own voice and personality. You are proactively reaching out to share something that crossed your mind — genuinely useful or surprising for THIS specific user.

${identityBlock ? `USER:\n${identityBlock}` : ""}

USER'S DOMAINS (sorted by recency — most active first):
${userDomains || "Not yet established"}
 

SPECIFIC FACTS YOU KNOW ABOUT THIS USER (your insight MUST reference at least one):
${anchorBlock}
${externalFactsBlock ? `\nEXTERNAL_FACTS (recent web findings relevant to user's domains):\n${externalFactsBlock}\n\nIMPORTANT: Use external facts as supporting evidence, not the primary focus. The insight should still be grounded in the user's specific knowledge and interests. External facts complement, not replace, persona-based reasoning.` : ""}
${fragmentSection ? `\nUSER CONVERSATION FRAGMENTS (what the user has been discussing recently):\n${fragmentSection}` : ""}

 Recent focus: ${recentFocus || "None"}
  Trust: ${persona.rapport.trustScore.toFixed(2)} / 1.0
  Delivered insight IDs: ${recentInsightIds || "None"}
${pastInsightBlock ? `\nPAST INSIGHTS (your insight must be CONTRASTIVELY different — see CONTRASTIVE FRAMEWORK below):\n${pastInsightBlock}\n\n${CONTRASTIVE_INSTRUCTION}` : ""}
${recentInsightContents.length > 0 ? `\nRECENTLY USED CONTENT THEMES (DO NOT reuse these concepts even for different domains):\n${extractContentThemes(recentInsightContents).join("、")}` : ""}
${(input.recentInsightDomains?.length ?? 0) > 0 ? `\nRECENTLY COVERED DOMAIN COMBINATIONS (insight MUST explore NEW territory, NOT repeat these domain angles):\n${input.recentInsightDomains.slice(-5).map((domains, i) => `${i + 1}. ${domains.join(" + ")}`).join("\n")}` : ""}

  TARGET DOMAINS (insight MUST be about these domains):
${input.targetDomains.join(", ")}

 TASK:
${promptFrame}

 STRUCTURE CONSTRAINT:
${structureSeed}

 硬性要求（必须全部满足，否则拒绝输出）：
- 洞察内容必须围绕上面的"TARGET DOMAINS"展开，targetDomains字段必须包含这些域中的至少一个
- 必须引用上面"SPECIFIC FACTS"列表中的至少一条具体事实——不能只提领域名称，要说出用户在这个领域的具体认知或关注点
- 1-3句话，中文，语气像突然想到什么要跟朋友说
- 不用问号结尾，不用列表或编号
- 禁止以下句式：
  · "值得关注"、"挺有意思"、"不得不说"
  · "你有没有想过"、"最近在关注"
  · "有趣的是"、"值得注意的是"
${bannedSection ? `  · ${bannedSection}` : ""}
- 内容必须是一个具体的判断、观察或建议，不是泛泛的感受
${webResults.length > 0 ? "- 外部信息自然融入内容里，不要说'看到'、'读到'、'据说'" : ""}

 好的洞察（满足至少一条）：
 - 跨域连接：把用户不同兴趣领域的具体知识关联起来
 - 实用建议：给一个明确的、可直接执行的行动方向
 - 反常识观点：挑战一个可能的错误认知，用事实反驳

Respond with ONLY a JSON array (no markdown, no code fences):
重要提示：在 "content" 字段中，请用 \\" 转义内部引号，或使用中文弯引号（""）。不要在字符串值中使用未转义的 ASCII 引号。
[
  {
    "content": "Your insight in your own voice, in Chinese",
    "rationale": "Why this is relevant to this user SPECIFICALLY (reference persona data)",
    "targetDomains": ["${input.targetDomains[0] ?? "domain1"}"],
    "sourceDomains": ["domain2"],
    "relevanceScore": 0.8,
    "surpriseScore": 0.6
  }
]
CRITICAL: targetDomains MUST include at least one of: ${input.targetDomains.join(", ")}. Do NOT substitute other domains.

Keep insights concise (1-3 sentences). Quality over quantity.`,
		variant: {
			fewShotSet: fewShotIdx,
			frameIndex,
			structureSeed: structureSeedIdx
		}
	};
}
function parseLLMInsights(text, maxCandidates) {
	try {
		const cleaned = text.replace(/^```(?:json)?\s*/m, "").replace(/\s*```$/m, "").trim();
		let jsonStr = extractJsonArray(cleaned);
		if (!jsonStr) {
			log.warn("parseLLMInsights: no JSON array found in LLM response", { raw: cleaned.slice(0, 200) });
			return [];
		}
		let parsed;
		try {
			parsed = JSON.parse(jsonStr);
		} catch {
			const repaired = repairJsonArray(jsonStr);
			try {
				parsed = JSON.parse(repaired);
			} catch {
				const aggressivelyRepaired = aggressiveAsciiQuoteRepair(repaired);
				try {
					parsed = JSON.parse(aggressivelyRepaired);
				} catch (repairErr) {
					log.warn("parseLLMInsights: JSON repair failed", {
						error: String(repairErr),
						raw: jsonStr.slice(0, 200)
					});
					return [];
				}
			}
		}
		return (Array.isArray(parsed) ? parsed : []).slice(0, maxCandidates).map((item) => ({
			id: randomUUID(),
			content: String(item.content ?? ""),
			rationale: String(item.rationale ?? ""),
			targetDomains: Array.isArray(item.targetDomains) ? item.targetDomains.map(String) : [],
			sourceDomains: Array.isArray(item.sourceDomains) ? item.sourceDomains.map(String) : [],
			relevanceScore: clamp01(Number(item.relevanceScore ?? .5)),
			surpriseScore: clamp01(Number(item.surpriseScore ?? .5)),
			compositeScore: (clamp01(Number(item.relevanceScore ?? .5)) + clamp01(Number(item.surpriseScore ?? .5))) / 2,
			sources: [],
			verificationStatus: "unverified",
			source: "knowledge"
		})).filter((c) => c.content.length > 0 && isSubstantiveContent(c.content));
	} catch (err) {
		log.warn("parseLLMInsights: unexpected error", { error: String(err) });
		return [];
	}
}
function extractJsonArray(text) {
	const start = text.indexOf("[");
	const end = text.lastIndexOf("]");
	if (start === -1 || end === -1 || end <= start) return null;
	return text.slice(start, end + 1);
}
function repairJsonArray(raw) {
	let s = raw;
	{
		let inStr = false;
		let esc = false;
		let normalized = "";
		for (let i = 0; i < s.length; i++) {
			const ch = s[i];
			if (esc) {
				normalized += ch;
				esc = false;
				continue;
			}
			if (ch === "\\") {
				normalized += ch;
				esc = true;
				continue;
			}
			if (ch === "\"") {
				inStr = !inStr;
				normalized += ch;
				continue;
			}
			if (inStr && (ch === "“" || ch === "”")) {
				normalized += "\"";
				continue;
			}
			normalized += ch;
		}
		s = normalized;
	}
	s = s.replace(/,\s*([}\]])/g, "$1");
	let openBrackets = 0;
	let openBraces = 0;
	let inString = false;
	let escape = false;
	for (const ch of s) {
		if (escape) {
			escape = false;
			continue;
		}
		if (ch === "\\") {
			escape = true;
			continue;
		}
		if (ch === "\"") {
			inString = !inString;
			continue;
		}
		if (inString) continue;
		if (ch === "[") openBrackets++;
		else if (ch === "]") openBrackets--;
		else if (ch === "{") openBraces++;
		else if (ch === "}") openBraces--;
	}
	while (openBraces > 0) {
		s += "}";
		openBraces--;
	}
	while (openBrackets > 0) {
		s += "]";
		openBrackets--;
	}
	return s;
}
/**
* Aggressive repair for unescaped ASCII `"` inside JSON string values.
* GLM models frequently produce output like:
*   [{"content": "他说"你好"吗", ...}]
* where the inner `"你好"` breaks JSON.parse.
*
* Strategy: character-by-character state machine that tracks whether we're
* inside a JSON string. When inside a string and encountering an unescaped `"`,
* we look ahead to decide if it's a structural quote (end-of-string) or an
* inner quote that needs escaping.
*/
function aggressiveAsciiQuoteRepair(raw) {
	let result = "";
	let i = 0;
	let inStr = false;
	let esc = false;
	while (i < raw.length) {
		const ch = raw[i];
		if (esc) {
			result += ch;
			esc = false;
			i++;
			continue;
		}
		if (ch === "\\") {
			result += ch;
			esc = true;
			i++;
			continue;
		}
		if (ch === "\"") {
			if (!inStr) {
				inStr = true;
				result += ch;
				i++;
				continue;
			}
			if (isStructuralQuote(raw, i)) {
				inStr = false;
				result += ch;
			} else result += "\\\"";
			i++;
			continue;
		}
		result += ch;
		i++;
	}
	return result;
}
/**
* Determine if the `"` at position `pos` in `raw` is a structural quote
* (i.e., terminates a JSON string value) rather than an inner quote.
*
* A `"` is structural if the next non-whitespace character is one of:
* `,` `}` `]` `:` — indicating the end of a string value in a JSON structure.
* Also structural if we're at end-of-string or end-of-input.
*/
function isStructuralQuote(raw, pos) {
	for (let j = pos + 1; j < raw.length; j++) {
		const next = raw[j];
		if (next === " " || next === "	" || next === "\n" || next === "\r") continue;
		return next === "," || next === "}" || next === "]" || next === ":";
	}
	return true;
}
const GENERIC_INSIGHT_PATTERNS = [
	/最近出现了?一些值得关注的新方向/,
	/结合你在这个领域的深度理解/,
	/可能会影响你的技术决策/,
	/探索未知领域有助于拓展思维边界/,
	/挺有意思的/,
	/值得关注/,
	/^.{0,10}是一个.{2,10}的方向$/,
	/被(人)?.*但(换个角度|它其实|它和)/,
	/你有没有想过/,
	/最近在关注/,
	/不得不说/,
	/其实.*也是$/,
	/背后.*值得/,
	/换个角度来看/,
	/有没有可能/,
	/有趣的是/,
	/值得注意的是/,
	/^关于.{2,6}[，,]/,
	/^在.{2,8}领域/,
	/^作为.{2,8}[，,]/,
	/结合你/
];
function isSubstantiveContent(content) {
	const trimmed = content.trim();
	if (trimmed.length < 10) return false;
	for (const pattern of GENERIC_INSIGHT_PATTERNS) if (pattern.test(trimmed)) return false;
	return true;
}
function clamp01(value) {
	return Math.max(0, Math.min(1, value));
}
const PERSONA_WORKSPACE_FILES = [
	"SOUL.md",
	"IDENTITY.md",
	"USER.md"
];
async function loadWorkspacePersonaContext(workspaceDir) {
	const dir = workspaceDir ?? path.join(os.homedir(), ".kaijibot", "workspace");
	const parts = [];
	for (const filename of PERSONA_WORKSPACE_FILES) try {
		const trimmed = (await fs.readFile(path.join(dir, filename), "utf-8")).trim();
		if (trimmed) parts.push(`## ${filename}\n${trimmed}`);
	} catch {}
	return parts.join("\n\n");
}
function buildCritiquePrompt(candidate, persona) {
	const anchorFacts = Object.entries(persona.domains).toSorted(([, a], [, b]) => b.lastMentioned - a.lastMentioned).flatMap(([name, d]) => getFilteredInsights(d).slice(0, 2).map((ki) => `${name}: ${ki}`)).slice(0, 6);
	const anchorBlock = anchorFacts.length > 0 ? anchorFacts.map((f, i) => `${i + 1}. ${f}`).join("\n") : "(not yet established)";
	return `You are a strict quality evaluator for AI-generated insights about a user.

USER: ${persona.identity?.displayName ?? "the user"}
EXPERT DOMAINS: ${persona.identity?.expertDomains?.join(", ") ?? "unknown"}
INTEREST DOMAINS: ${persona.identity?.interestDomains?.join(", ") ?? "unknown"}

KNOWN FACTS ABOUT THIS USER:
${anchorBlock}

INSIGHT TO EVALUATE:
---
${candidate.content}
---
Target domains: ${candidate.targetDomains.join(", ")}
Rationale: ${candidate.rationale}
---

Evaluate this insight on 5 dimensions (each 0.0-1.0):

1. SPECIFICITY: Does the insight contain concrete, verifiable claims? Or is it vague platitudes?
2. PERSONA RELEVANCE: Does it reference known facts about THIS user? Or generic advice anyone could receive?
3. ACTIONABILITY: Can the user act on this? Or is it an abstract observation with no next step?
4. SURPRISE: Is this genuinely new information the user likely doesn't know? Or obvious/common knowledge?
5. VOICE MATCH: Does it sound natural for a helpful AI companion? Or stiff/formal/system-notification-like?

Also provide an overallScore (0.0-1.0), a textual critique, and specific improvement suggestions.

Respond with ONLY a JSON object (no markdown, no code fences):
{
  "specificity": 0.0-1.0,
  "personaRelevance": 0.0-1.0,
  "actionability": 0.0-1.0,
  "surprise": 0.0-1.0,
  "voiceMatch": 0.0-1.0,
  "overallScore": 0.0-1.0,
  "critique": "textual feedback explaining the scores",
  "improvementSuggestions": ["specific suggestion 1", "specific suggestion 2"]
}`;
}
function buildRefinePrompt(originalPrompt, candidate, critique, persona) {
	const suggestions = critique.improvementSuggestions.map((s, i) => `${i + 1}. ${s}`).join("\n");
	return `${buildVoiceSection(persona)}

ORIGINAL GENERATION PROMPT:
---
${originalPrompt}
---

ORIGINAL INSIGHT:
---
${candidate.content}
---

CRITIQUE (overall score: ${critique.overallScore.toFixed(2)}/1.0):
${critique.critique}

IMPROVEMENT SUGGESTIONS:
${suggestions}

Generate a REVISED insight that addresses these specific weaknesses. Keep the strengths, fix the problems. The revised insight should feel like it could ONLY be about THIS specific user.

Constraints:
- 1-3 sentences, Chinese
- No question marks, no lists, no numbering
- Forbidden phrases: "值得关注", "挺有意思", "不得不说", "你有没有想过", "最近在关注", "有趣的是", "值得注意的是"
- Start with a concrete fact, observation, or judgment — never with "关于", "在...领域", "结合你", "作为"

Respond with ONLY a JSON array (no markdown, no code fences):
[
  {
    "content": "Your revised insight in your own voice, in Chinese",
    "rationale": "Why this revision is better",
    "targetDomains": ${JSON.stringify(candidate.targetDomains)},
    "sourceDomains": ${JSON.stringify(candidate.sourceDomains)},
    "relevanceScore": 0.8,
    "surpriseScore": 0.7
  }
]`;
}
async function critiqueInsightWithLLM(candidate, persona, config, deps, options) {
	try {
		const prompt = buildCritiquePrompt(candidate, persona);
		const modelRef = options?.modelRef ?? config.cognitive?.persona?.extractionModel;
		const prepared = await deps.prepareModel(config, modelRef);
		if ("error" in prepared) {
			log.warn("critiqueInsightWithLLM: model preparation failed", { error: prepared.error });
			return null;
		}
		const text = (await deps.complete(prepared.model, { messages: [{
			role: "user",
			content: prompt,
			timestamp: Date.now()
		}] }, {
			apiKey: prepared.auth.apiKey,
			maxTokens: options?.maxTokens ?? 500,
			temperature: .3,
			signal: AbortSignal.timeout(options?.timeout ?? 8e3)
		})).content.filter((block) => block.type === "text").map((block) => block.text).join("").trim();
		if (!text) return null;
		const objStart = text.indexOf("{");
		const objEnd = text.lastIndexOf("}");
		if (objStart === -1 || objEnd === -1 || objEnd <= objStart) return null;
		const parsed = JSON.parse(text.slice(objStart, objEnd + 1));
		for (const field of [
			"specificity",
			"personaRelevance",
			"actionability",
			"surprise",
			"voiceMatch",
			"overallScore",
			"critique",
			"improvementSuggestions"
		]) if (!(field in parsed)) return null;
		const improvementSuggestions = parsed.improvementSuggestions;
		if (!Array.isArray(improvementSuggestions)) return null;
		return {
			specificity: clamp01(Number(parsed.specificity) || 0),
			personaRelevance: clamp01(Number(parsed.personaRelevance) || 0),
			actionability: clamp01(Number(parsed.actionability) || 0),
			surprise: clamp01(Number(parsed.surprise) || 0),
			voiceMatch: clamp01(Number(parsed.voiceMatch) || 0),
			overallScore: clamp01(Number(parsed.overallScore) || 0),
			critique: String(parsed.critique ?? ""),
			improvementSuggestions: improvementSuggestions.map(String)
		};
	} catch (err) {
		log.warn("critiqueInsightWithLLM: failed", { error: String(err) });
		return null;
	}
}
async function refineInsightWithLLM(originalPrompt, candidate, critique, persona, config, deps, options) {
	try {
		const prompt = buildRefinePrompt(originalPrompt, candidate, critique, persona);
		const modelRef = options?.modelRef ?? config.cognitive?.persona?.extractionModel;
		const prepared = await deps.prepareModel(config, modelRef);
		if ("error" in prepared) {
			log.warn("refineInsightWithLLM: model preparation failed", { error: prepared.error });
			return null;
		}
		const text = (await deps.complete(prepared.model, { messages: [{
			role: "user",
			content: prompt,
			timestamp: Date.now()
		}] }, {
			apiKey: prepared.auth.apiKey,
			maxTokens: options?.maxTokens ?? 500,
			temperature: .85,
			signal: AbortSignal.timeout(options?.timeout ?? 8e3)
		})).content.filter((block) => block.type === "text").map((block) => block.text).join("").trim();
		if (!text) return null;
		const candidates = parseLLMInsights(text, 1);
		if (candidates.length === 0) return null;
		return {
			...candidates[0],
			id: candidate.id,
			targetDomains: candidate.targetDomains,
			sources: candidate.sources,
			promptVariant: candidate.promptVariant
		};
	} catch (err) {
		log.warn("refineInsightWithLLM: failed", { error: String(err) });
		return null;
	}
}
function buildVerificationPrompt(candidate, persona) {
	const anchorFacts = Object.entries(persona.domains).toSorted(([, a], [, b]) => b.lastMentioned - a.lastMentioned).flatMap(([name, d]) => getFilteredInsights(d).slice(0, 2).map((ki) => `${name}: ${ki}`)).slice(0, 6);
	const anchorBlock = anchorFacts.length > 0 ? anchorFacts.map((f, i) => `${i + 1}. ${f}`).join("\n") : "(not yet established)";
	const sourceBlock = candidate.sources.length > 0 ? candidate.sources.map((s, i) => `${i + 1}. [${s.title}](${s.url}) (credibility: ${s.credibility})`).join("\n") : "(no sources)";
	return `You are a quality gate judge for AI-generated proactive insights.

USER: ${persona.identity?.displayName ?? "the user"}
EXPERT DOMAINS: ${persona.identity?.expertDomains?.join(", ") ?? "unknown"}
INTEREST DOMAINS: ${persona.identity?.interestDomains?.join(", ") ?? "unknown"}

KNOWN FACTS:
${anchorBlock}

INSIGHT TO VERIFY:
---
${candidate.content}
---
Target domains: ${candidate.targetDomains.join(", ")}
Rationale: ${candidate.rationale}

SOURCES:
${sourceBlock}
---

Evaluate: Is this insight worth delivering to the user?

Criteria for each status:
- "verified": High quality — specific, relevant to THIS user, actionable, consistent with sources
- "partial": Decent quality but missing some elements — still acceptable for delivery
- "unverified": Generic, vague, or not relevant enough to this specific user
- "contradicted": Contains factual errors or contradicts known information about the user

Respond with ONLY a JSON object (no markdown, no code fences):
{
  "approved": true/false,
  "confidence": 0.0-1.0,
  "status": "verified" | "partial" | "unverified" | "contradicted",
  "notes": "Brief explanation of the verdict"
}`;
}
async function verifyInsightWithLLM(candidate, persona, config, deps, options) {
	const unverified = {
		status: "unverified",
		sources: candidate.sources,
		confidence: 0,
		notes: "Verification unavailable"
	};
	try {
		const prompt = buildVerificationPrompt(candidate, persona);
		const modelRef = options?.modelRef ?? config.cognitive?.persona?.extractionModel;
		const prepared = await deps.prepareModel(config, modelRef);
		if ("error" in prepared) {
			log.warn("verifyInsightWithLLM: model preparation failed", { error: prepared.error });
			return unverified;
		}
		const text = (await deps.complete(prepared.model, { messages: [{
			role: "user",
			content: prompt,
			timestamp: Date.now()
		}] }, {
			apiKey: prepared.auth.apiKey,
			maxTokens: options?.maxTokens ?? 300,
			temperature: .2,
			signal: AbortSignal.timeout(options?.timeout ?? 8e3)
		})).content.filter((block) => block.type === "text").map((block) => block.text).join("").trim();
		if (!text) return unverified;
		const objStart = text.indexOf("{");
		const objEnd = text.lastIndexOf("}");
		if (objStart === -1 || objEnd === -1 || objEnd <= objStart) return unverified;
		const parsed = JSON.parse(text.slice(objStart, objEnd + 1));
		const confidence = clamp01(Number(parsed.confidence) || 0);
		const llmStatus = String(parsed.status ?? "");
		let status;
		if (llmStatus === "contradicted") status = "contradicted";
		else if (confidence >= .7) status = "verified";
		else if (confidence >= .4) status = "partial";
		else status = "unverified";
		return {
			status,
			sources: candidate.sources,
			confidence,
			notes: String(parsed.notes ?? "")
		};
	} catch (err) {
		log.warn("verifyInsightWithLLM: failed", { error: String(err) });
		return unverified;
	}
}
function buildFreshnessPrompt(candidate, recentInsightContents) {
	const MAX_PER_INSIGHT = 120;
	const shown = recentInsightContents.slice(0, 5);
	const pastBlock = shown.map((text, i) => `${i + 1}. ${text.length > MAX_PER_INSIGHT ? text.slice(0, MAX_PER_INSIGHT) : text}`).join("\n");
	return `SYSTEM: You are a semantic novelty evaluator. Your job is to determine if a new insight says something genuinely new compared to past insights.

NEW INSIGHT:
${candidate.content}

PAST INSIGHTS (last ${shown.length}):
${pastBlock}

Is this new insight semantically equivalent to or a paraphrase of any past insight? Or does it say something genuinely new?

Respond ONLY with valid JSON:
{ "isNovel": boolean, "similarityToClosest": 0-1, "reason": string }

Criteria:
- isNovel = true: the insight covers genuinely different ground, introduces a new angle, or connects ideas in a way not seen in past insights.
- isNovel = false: the insight is semantically the same as a past insight even if worded differently. Paraphrases, restatements, and near-duplicates should be marked as not novel.
- similarityToClosest: 0 = completely different topic, 1 = essentially the same insight.
- reason: one concise sentence explaining your decision.`;
}
async function checkSemanticNoveltyWithLLM(candidate, recentInsightContents, config, deps, options) {
	if (recentInsightContents.length < 2) return {
		isNovel: true,
		reason: "Insufficient history for comparison"
	};
	try {
		const prompt = buildFreshnessPrompt(candidate, recentInsightContents);
		const modelRef = options?.modelRef ?? config.cognitive?.persona?.extractionModel;
		const prepared = await deps.prepareModel(config, modelRef);
		if ("error" in prepared) {
			log.warn("checkSemanticNoveltyWithLLM: model preparation failed", { error: prepared.error });
			return {
				isNovel: true,
				reason: "LLM freshness check unavailable"
			};
		}
		const text = (await deps.complete(prepared.model, { messages: [{
			role: "user",
			content: prompt,
			timestamp: Date.now()
		}] }, {
			apiKey: prepared.auth.apiKey,
			maxTokens: options?.maxTokens ?? 200,
			temperature: .2,
			signal: AbortSignal.timeout(options?.timeout ?? 6e3)
		})).content.filter((block) => block.type === "text").map((block) => block.text).join("").trim();
		if (!text) return {
			isNovel: true,
			reason: "LLM freshness check unavailable"
		};
		const objStart = text.indexOf("{");
		const objEnd = text.lastIndexOf("}");
		if (objStart === -1 || objEnd === -1 || objEnd <= objStart) return {
			isNovel: true,
			reason: "LLM freshness check unavailable"
		};
		const parsed = JSON.parse(text.slice(objStart, objEnd + 1));
		if (typeof parsed.isNovel !== "boolean" || typeof parsed.reason !== "string") return {
			isNovel: true,
			reason: "LLM freshness check unavailable"
		};
		return {
			isNovel: parsed.isNovel,
			reason: parsed.reason
		};
	} catch (err) {
		log.warn("checkSemanticNoveltyWithLLM: failed", { error: String(err) });
		return {
			isNovel: true,
			reason: "LLM freshness check unavailable"
		};
	}
}
//#endregion
export { isDuplicateBySemanticOverlap as S, isSubstantiveContent as _, buildPatternInsightPrompt as a, verifyInsightWithLLM as b, buildSurpriseInsightPrompt as c, checkSemanticNoveltyWithLLM as d, createDefaultInsightDeps as f, getFilteredInsights as g, generateInsightCandidatesLLM as h, buildInsightPrompt as i, buildVerificationPrompt as l, extractKeyTerms as m, GENERIC_INSIGHT_PATTERNS as n, buildRefinePrompt as o, critiqueInsightWithLLM as p, buildCritiquePrompt as r, buildSearchQuery as s, CONTRASTIVE_INSTRUCTION as t, buildVoiceSection as u, loadWorkspacePersonaContext as v, computeTrigramSimilarity as x, refineInsightWithLLM as y };
