import "./paths-CE7eVGHg.js";
import "./registry-C-JddWwo.js";
import { t as createSubsystemLogger } from "./subsystem-DzRUKS9f.js";
import { l as parseEmbedding, r as cosineSimilarity } from "./internal-BYHsj5p6.js";
import path from "node:path";
import fs from "node:fs/promises";
import crypto from "node:crypto";

//#region src/memory/consolidate.ts
const log = createSubsystemLogger("memory/consolidate");
/**
* L3 long-term memory cycle. Runs autonomously on a schedule or via
* `symi memory consolidate` (manual). Operates over the existing L2
* SQLite store; does not introduce a new substrate.
*
* Six stages, always in this order:
*   1. Decay       — kind-specific exponential decay on `importance`.
*   2. Reinforce   — new chunks boost existing similar chunks.
*   3. Propagate   — `importance` flows through `edges` (one hop).
*   4. Synthesize  — cluster observations → belief text (in synthesis.ts).
*   5. Promote     — rewrite memory/symi-core.md + memory/symi-beliefs.md.
*   6. Prune       — hard-delete low-importance aged unpinned unpromoted
*                    chunks with no incident edges.
*
* Autonomy is bounded by `minIntervalMs` in `runL3CycleIfDue` so rapid
* successive flushes don't stack. Every stage is exported for isolated
* testing.
*
* Beliefs live in the filesystem (`memory/symi-beliefs.md`), NOT as
* separate DB chunks. On the next sync, the file is chunked normally and
* becomes searchable via the regular L2 pipeline. This avoids the
* duplicate-chunk problem that arises if L3 inserts its own rows alongside
* file-derived ones.
*/
const CHUNK_KIND_SAFE = {
	observation: "observation",
	belief: "belief",
	preference: "preference",
	fact: "fact",
	lesson: "lesson",
	identity: "identity"
};
/**
* Exponential-decay half-life (days) per chunk kind. Longer half-life =
* slower forgetting. Used in stage 1.
*/
const KIND_HALF_LIFE_DAYS = {
	observation: 60,
	fact: 180,
	preference: 365,
	lesson: 365,
	belief: 540,
	identity: 730
};
/**
* Ranking multiplier per kind in stage 5 (promotion). Higher = prefers
* kind when selecting top-N for a section.
*/
const KIND_WEIGHT = {
	identity: 1.5,
	preference: 1.4,
	belief: 1.3,
	lesson: 1.2,
	fact: 1.1,
	observation: 1
};
/** Cosine-similarity threshold for stage 2 reinforcement. */
const REINFORCE_SIMILARITY_THRESHOLD = .85;
/** Edge-hop propagation factor (stage 3). Conservative to avoid runaway feedback. */
const PROPAGATION_FACTOR = .1;
/** Stage 6 pruning criteria. All must hold. */
const PRUNE_MIN_IMPORTANCE = .2;
const PRUNE_MIN_AGE_DAYS = 90;
/** Stage 5 caps so the visible L1 files don't balloon. */
const SYMI_CORE_MAX_BULLETS_PER_SECTION = 15;
const SYMI_BELIEFS_MAX = 40;
/** Stage 4 clustering defaults. */
const CLUSTER_MUTUAL_THRESHOLD = .75;
const CLUSTER_MIN_SIZE = 3;
const CLUSTER_MIN_IMPORTANCE = 1;
/**
* Minimum gap between autonomous cycles. Post-flush may fire many times
* in rapid succession; this guard avoids back-to-back pipeline runs.
*/
const DEFAULT_MIN_INTERVAL_MS = 600 * 1e3;
const META_LAST_L3_CYCLE_AT = "l3_last_cycle_at";
const SYMI_CORE_PATH = "memory/symi-core.md";
const SYMI_BELIEFS_PATH = "memory/symi-beliefs.md";
/**
* Apply exponential decay based on age since last access. Pinned chunks
* are exempt. Kinds with longer half-lives decay slower.
*
* Formula: importance *= exp(-ln(2) / halfLifeDays * ageDays)
*/
function stageDecay(params) {
	const now = params.now ?? Date.now();
	const halfLives = {
		...KIND_HALF_LIFE_DAYS,
		...params.halfLifeDays
	};
	const rows = params.db.prepare(`SELECT id, kind, importance, pinned, last_accessed_at, updated_at
       FROM chunks
       WHERE pinned = 0`).all();
	if (rows.length === 0) return { decayed: 0 };
	const update = params.db.prepare(`UPDATE chunks SET importance = ? WHERE id = ?`);
	let decayed = 0;
	for (const row of rows) {
		const referenceTs = row.last_accessed_at > 0 ? row.last_accessed_at : row.updated_at;
		if (referenceTs <= 0) continue;
		const ageDays = Math.max(0, (now - referenceTs) / 864e5);
		if (ageDays <= 0) continue;
		const halfLife = halfLives[CHUNK_KIND_SAFE[row.kind] ?? "observation"] ?? KIND_HALF_LIFE_DAYS.observation;
		if (halfLife <= 0) continue;
		const lambda = Math.LN2 / halfLife;
		const multiplier = Math.exp(-lambda * ageDays);
		if (multiplier >= .999) continue;
		const next = Math.max(0, row.importance * multiplier);
		update.run(next, row.id);
		decayed += 1;
	}
	return { decayed };
}
/**
* For chunks created since the previous cycle, find semantically similar
* older chunks. Each hit bumps the older chunk's reinforcement_count and
* boosts its importance by a diminishing-returns log factor.
*
* The new chunk itself is unchanged — it stays a fresh observation. Only
* older chunks that "heard the same thing again" get strengthened. This
* is how repeated themes compound into durable beliefs.
*/
function stageReinforce(params) {
	const threshold = params.threshold ?? REINFORCE_SIMILARITY_THRESHOLD;
	const newRows = params.db.prepare(`SELECT id, embedding, importance, reinforcement_count, updated_at
       FROM chunks
       WHERE updated_at >= ? AND embedding IS NOT NULL AND embedding != ''`).all(params.sinceTs);
	if (newRows.length === 0) return { reinforced: 0 };
	const olderRows = params.db.prepare(`SELECT id, embedding, importance, reinforcement_count, updated_at
       FROM chunks
       WHERE updated_at < ? AND embedding IS NOT NULL AND embedding != ''`).all(params.sinceTs);
	if (olderRows.length === 0) return { reinforced: 0 };
	const update = params.db.prepare(`UPDATE chunks
     SET reinforcement_count = reinforcement_count + 1,
         importance = importance + ?
     WHERE id = ?`);
	const olderParsed = olderRows.map((row) => ({
		id: row.id,
		vec: parseEmbedding(row.embedding),
		reinforcement_count: row.reinforcement_count
	}));
	let reinforced = 0;
	for (const fresh of newRows) {
		const freshVec = parseEmbedding(fresh.embedding);
		if (freshVec.length === 0) continue;
		for (const older of olderParsed) {
			if (older.vec.length === 0 || older.id === fresh.id) continue;
			const sim = cosineSimilarity(freshVec, older.vec);
			if (sim < threshold) continue;
			const boost = Math.log(older.reinforcement_count + 2) * sim * .1;
			update.run(boost, older.id);
			older.reinforcement_count += 1;
			reinforced += 1;
		}
	}
	return { reinforced };
}
/**
* One-hop edge propagation: each chunk's importance gains a fraction of
* its neighbours' importance. Uses the existing `edges` table.
*
* Computed as a single SQL aggregate so we don't round-trip per-chunk.
*/
function stagePropagate(params) {
	const factor = params.factor ?? PROPAGATION_FACTOR;
	const deltas = params.db.prepare(`SELECT e.target_chunk_id AS id,
              SUM(e.weight * c.importance) AS delta
       FROM edges e
       JOIN chunks c ON c.id = e.source_chunk_id
       GROUP BY e.target_chunk_id`).all();
	if (deltas.length === 0) return { propagated: 0 };
	const update = params.db.prepare(`UPDATE chunks SET importance = importance + ? WHERE id = ?`);
	let propagated = 0;
	for (const row of deltas) {
		if (!row.id || !Number.isFinite(row.delta) || row.delta <= 0) continue;
		update.run(row.delta * factor, row.id);
		propagated += 1;
	}
	return { propagated };
}
/**
* Find clusters of highly-similar high-importance observation chunks.
* Each cluster is a connected component of the "cosine > mutualThreshold"
* graph restricted to observations that have not yet been promoted.
*
* Clusters smaller than `minSize` are discarded.
*/
function findSynthesisClusters(params) {
	const mutualThreshold = params.mutualThreshold ?? CLUSTER_MUTUAL_THRESHOLD;
	const minSize = params.minSize ?? CLUSTER_MIN_SIZE;
	const minImportance = params.minImportance ?? CLUSTER_MIN_IMPORTANCE;
	const rows = params.db.prepare(`SELECT id, text, importance, embedding
       FROM chunks
       WHERE kind = 'observation'
         AND pinned = 0
         AND promoted_to IS NULL
         AND importance >= ?
         AND embedding IS NOT NULL AND embedding != ''`).all(minImportance);
	if (rows.length < minSize) return [];
	const parsed = rows.map((row) => ({
		id: row.id,
		text: row.text,
		importance: row.importance,
		embedding: parseEmbedding(row.embedding)
	}));
	const parent = /* @__PURE__ */ new Map();
	for (const p of parsed) parent.set(p.id, p.id);
	const find = (x) => {
		let cur = x;
		while (parent.get(cur) !== cur) {
			const next = parent.get(cur);
			if (!next) break;
			parent.set(cur, parent.get(next) ?? next);
			cur = next;
		}
		return cur;
	};
	const union = (a, b) => {
		const ra = find(a);
		const rb = find(b);
		if (ra !== rb) parent.set(ra, rb);
	};
	for (let i = 0; i < parsed.length; i += 1) for (let j = i + 1; j < parsed.length; j += 1) {
		const a = parsed[i];
		const b = parsed[j];
		if (!a || !b || a.embedding.length === 0 || b.embedding.length === 0) continue;
		if (cosineSimilarity(a.embedding, b.embedding) >= mutualThreshold) union(a.id, b.id);
	}
	const groups = /* @__PURE__ */ new Map();
	for (const p of parsed) {
		const root = find(p.id);
		const list = groups.get(root) ?? [];
		list.push(p);
		groups.set(root, list);
	}
	const out = [];
	for (const members of groups.values()) if (members.length >= minSize) out.push({ members });
	return out;
}
/**
* Run stage 4. Calls the caller-supplied synthesize function for each
* cluster and marks source observations as promoted so they don't
* re-cluster next cycle. Returns synthesized beliefs in memory — the
* caller (stage 5) is responsible for writing them to disk.
*/
async function runSynthesisStage(params) {
	const clusters = findSynthesisClusters({
		db: params.db,
		mutualThreshold: params.overrides?.clusterMutualThreshold,
		minSize: params.overrides?.clusterMinSize,
		minImportance: params.overrides?.clusterMinImportance
	});
	if (clusters.length === 0) return {
		beliefs: [],
		clustersSynthesized: 0
	};
	const markPromoted = params.db.prepare(`UPDATE chunks SET promoted_to = ? WHERE id = ?`);
	const beliefs = [];
	let clustersSynthesized = 0;
	for (const cluster of clusters) {
		const result = await params.synthesize({ sources: cluster.members.map((m) => ({
			id: m.id,
			text: m.text,
			importance: m.importance
		})) }).catch((err) => {
			log.warn(`synthesis: cluster failed: ${String(err)}`);
			return null;
		});
		clustersSynthesized += 1;
		if (!result || !result.text.trim()) continue;
		const sourceIds = cluster.members.map((m) => m.id);
		beliefs.push({
			text: result.text.trim(),
			kind: result.kind,
			confidence: cluster.members.length,
			sourceIds
		});
		for (const id of sourceIds) try {
			markPromoted.run(SYMI_BELIEFS_PATH, id);
		} catch (err) {
			log.warn(`synthesis: mark promoted failed for ${id}: ${String(err)}`);
		}
	}
	return {
		beliefs,
		clustersSynthesized
	};
}
const CORE_SECTIONS = [
	{
		key: "identity",
		heading: "Who I am to this user",
		kinds: ["identity"]
	},
	{
		key: "aboutUser",
		heading: "What I know about the user",
		kinds: ["fact", "preference"]
	},
	{
		key: "workingTogether",
		heading: "How we work together",
		kinds: ["preference", "lesson"]
	},
	{
		key: "beliefs",
		heading: "Durable beliefs",
		kinds: ["belief"]
	},
	{
		key: "lessons",
		heading: "Lessons",
		kinds: ["lesson"]
	}
];
/**
* Regenerate `memory/symi-core.md` and `memory/symi-beliefs.md`.
*
* - symi-core.md is a full rewrite from top-ranked chunks, grouped by
*   section. Hand edits are clobbered; that's deliberate — it's a
*   projection of DB state.
*
* - symi-beliefs.md is a merge: existing beliefs parsed from the file,
*   new beliefs from stage 4 added (dedupe by text hash), sorted by
*   confidence, capped at SYMI_BELIEFS_MAX.
*
* Promoted chunks get marked with `promoted_to = <path>` so stage 6
* knows not to prune them.
*/
async function stagePromote(params) {
	const maxPerSection = params.maxBulletsPerSection ?? SYMI_CORE_MAX_BULLETS_PER_SECTION;
	const maxBeliefs = params.maxBeliefs ?? SYMI_BELIEFS_MAX;
	const kindWeights = {
		...KIND_WEIGHT,
		...params.kindWeight
	};
	const now = params.now ?? Date.now();
	const memoryDir = path.join(params.workspaceDir, "memory");
	await fs.mkdir(memoryDir, { recursive: true }).catch(() => {});
	const corePath = path.join(memoryDir, "symi-core.md");
	const beliefsPath = path.join(memoryDir, "symi-beliefs.md");
	params.db.prepare(`UPDATE chunks SET promoted_to = NULL WHERE promoted_to = ?`).run(SYMI_CORE_PATH);
	const sectionLines = /* @__PURE__ */ new Map();
	const markPromoted = params.db.prepare(`UPDATE chunks SET promoted_to = ? WHERE id = ?`);
	const promotedIds = /* @__PURE__ */ new Set();
	let promoted = 0;
	for (const section of CORE_SECTIONS) {
		const placeholders = section.kinds.map(() => "?").join(",");
		const scored = params.db.prepare(`SELECT id, text, kind, importance, reinforcement_count, path, start_line, end_line
         FROM chunks
         WHERE kind IN (${placeholders})
           AND source = 'memory'
           AND (superseded_by IS NULL OR superseded_by = '')
         ORDER BY importance DESC`).all(...section.kinds).map((row) => {
			const kind = CHUNK_KIND_SAFE[row.kind] ?? "observation";
			return {
				row,
				score: row.importance * (kindWeights[kind] ?? 1)
			};
		});
		scored.sort((a, b) => b.score - a.score);
		const lines = [];
		for (const pick of scored) {
			if (lines.length >= maxPerSection) break;
			if (promotedIds.has(pick.row.id)) continue;
			const bullet = extractFirstBullet(pick.row.text) ?? firstSentence(pick.row.text);
			if (!bullet) continue;
			lines.push(`- ${bullet} <!-- chunk:${pick.row.id} -->`);
			markPromoted.run(SYMI_CORE_PATH, pick.row.id);
			promotedIds.add(pick.row.id);
			promoted += 1;
		}
		sectionLines.set(section.key, lines);
	}
	const symiCoreContent = buildSymiCoreMd({
		sections: sectionLines,
		now
	});
	await fs.writeFile(corePath, symiCoreContent, "utf-8");
	const merged = mergeBeliefs({
		existing: await readExistingBeliefs(beliefsPath),
		fresh: params.synthesizedBeliefs ?? [],
		cap: maxBeliefs
	});
	const symiBeliefsContent = buildSymiBeliefsMd({
		beliefs: merged,
		now
	});
	await fs.writeFile(beliefsPath, symiBeliefsContent, "utf-8");
	log.debug("consolidate.promote", {
		promoted,
		sections: CORE_SECTIONS.length,
		beliefs: merged.length
	});
	return {
		promoted,
		paths: {
			symiCore: SYMI_CORE_PATH,
			symiBeliefs: SYMI_BELIEFS_PATH
		}
	};
}
/**
* Hard-delete chunks that meet ALL criteria. Conservative on purpose —
* L1 is the source of truth, so next sync re-ingests anything wrongly
* pruned. Cascades across the chunk's companion vec/fts rows.
*/
function stagePrune(params) {
	const now = params.now ?? Date.now();
	const minImportance = params.minImportance ?? PRUNE_MIN_IMPORTANCE;
	const cutoff = now - (params.minAgeDays ?? PRUNE_MIN_AGE_DAYS) * 864e5;
	const candidates = params.db.prepare(`SELECT id FROM chunks
       WHERE pinned = 0
         AND promoted_to IS NULL
         AND importance < ?
         AND updated_at < ?
         AND id NOT IN (SELECT source_chunk_id FROM edges)
         AND id NOT IN (SELECT target_chunk_id FROM edges)`).all(minImportance, cutoff);
	if (candidates.length === 0) return { pruned: 0 };
	const delChunk = params.db.prepare(`DELETE FROM chunks WHERE id = ?`);
	const delVec = params.vectorTable ? params.db.prepare(`DELETE FROM ${params.vectorTable} WHERE id = ?`) : null;
	const delFts = params.ftsTable ? params.db.prepare(`DELETE FROM ${params.ftsTable} WHERE id = ?`) : null;
	let pruned = 0;
	for (const row of candidates) try {
		if (delVec) try {
			delVec.run(row.id);
		} catch {}
		if (delFts) try {
			delFts.run(row.id);
		} catch {}
		delChunk.run(row.id);
		pruned += 1;
	} catch (err) {
		log.warn(`prune: failed to delete chunk ${row.id}: ${String(err)}`);
	}
	return { pruned };
}
async function runL3Cycle(params) {
	const started = Date.now();
	const sinceTs = readLastCycleAt(params.db);
	const overrides = params.overrides ?? {};
	const decay = stageDecay({
		db: params.db,
		now: started,
		halfLifeDays: overrides.halfLifeDays
	});
	const reinforce = stageReinforce({
		db: params.db,
		sinceTs,
		threshold: overrides.reinforceThreshold
	});
	const propagate = stagePropagate({
		db: params.db,
		factor: overrides.propagationFactor
	});
	let clustersSynthesized = 0;
	let beliefs = [];
	if (params.synthesize) {
		const result = await runSynthesisStage({
			db: params.db,
			synthesize: params.synthesize,
			overrides: {
				clusterMutualThreshold: overrides.clusterMutualThreshold,
				clusterMinSize: overrides.clusterMinSize,
				clusterMinImportance: overrides.clusterMinImportance
			}
		});
		clustersSynthesized = result.clustersSynthesized;
		beliefs = result.beliefs;
	}
	const promotion = await stagePromote({
		db: params.db,
		workspaceDir: params.workspaceDir,
		synthesizedBeliefs: beliefs,
		maxBulletsPerSection: overrides.maxBulletsPerSection,
		maxBeliefs: overrides.maxBeliefs,
		kindWeight: overrides.kindWeight,
		now: started
	});
	const prune = stagePrune({
		db: params.db,
		now: started,
		minImportance: overrides.pruneMinImportance,
		minAgeDays: overrides.pruneMinAgeDays
	});
	writeLastCycleAt(params.db, started);
	const result = {
		ranAt: started,
		durationMs: Date.now() - started,
		decayed: decay.decayed,
		reinforced: reinforce.reinforced,
		propagated: propagate.propagated,
		clustersSynthesized,
		beliefsCreated: beliefs.length,
		promoted: promotion.promoted,
		pruned: prune.pruned,
		paths: promotion.paths
	};
	log.debug("l3.cycle", result);
	return result;
}
async function runL3CycleIfDue(params) {
	const minInterval = params.minIntervalMs ?? DEFAULT_MIN_INTERVAL_MS;
	const last = readLastCycleAt(params.db);
	const now = Date.now();
	if (last > 0 && now - last < minInterval) return {
		ranAt: now,
		durationMs: 0,
		decayed: 0,
		reinforced: 0,
		propagated: 0,
		clustersSynthesized: 0,
		beliefsCreated: 0,
		promoted: 0,
		pruned: 0,
		paths: {},
		skipped: { reason: `min interval ${minInterval}ms not elapsed` }
	};
	return runL3Cycle(params);
}
function readLastCycleAt(db) {
	try {
		const row = db.prepare(`SELECT value FROM meta WHERE key = ?`).get(META_LAST_L3_CYCLE_AT);
		if (!row) return 0;
		const parsed = Number.parseInt(row.value, 10);
		return Number.isFinite(parsed) ? parsed : 0;
	} catch {
		return 0;
	}
}
function writeLastCycleAt(db, ts) {
	try {
		db.prepare(`INSERT INTO meta (key, value) VALUES (?, ?)
       ON CONFLICT (key) DO UPDATE SET value = excluded.value`).run(META_LAST_L3_CYCLE_AT, String(ts));
	} catch (err) {
		log.warn(`l3: could not persist last-cycle timestamp: ${String(err)}`);
	}
}
function extractFirstBullet(text) {
	for (const raw of text.split("\n")) {
		const line = raw.trim();
		if (line.startsWith("- ") || line.startsWith("* ")) return line.replace(/^[-*]\s+/, "").trim();
	}
	return null;
}
function firstSentence(text) {
	const trimmed = text.trim();
	if (trimmed.length < 10) return null;
	const candidate = trimmed.match(/^(.+?[.!?])(\s|$)/)?.[1]?.trim() ?? trimmed.slice(0, 200).trim();
	return candidate.length >= 10 ? candidate : null;
}
function buildSymiCoreMd(params) {
	const stamp = new Date(params.now).toISOString();
	const lines = [];
	lines.push("# Symi Core Memory");
	lines.push("");
	lines.push(`<!-- generated by L3 consolidation on ${stamp}; do not hand-edit -->`);
	lines.push("<!-- this file is rewritten each cycle from indexed long-term memory; edits will be lost -->");
	lines.push("");
	for (const section of CORE_SECTIONS) {
		lines.push(`## ${section.heading}`);
		const bullets = params.sections.get(section.key) ?? [];
		if (bullets.length === 0) lines.push("- (none yet)");
		else for (const b of bullets) lines.push(b);
		lines.push("");
	}
	return lines.join("\n");
}
function mergeBeliefs(params) {
	const byHash = /* @__PURE__ */ new Map();
	for (const b of params.existing) byHash.set(b.hash, b);
	for (const fresh of params.fresh) {
		const hash = hashNormalizedText(fresh.text);
		const existing = byHash.get(hash);
		if (existing) byHash.set(hash, {
			text: existing.text,
			confidence: existing.confidence + fresh.confidence,
			hash
		});
		else byHash.set(hash, {
			text: fresh.text,
			confidence: fresh.confidence,
			hash
		});
	}
	return [...byHash.values()].toSorted((a, b) => b.confidence - a.confidence).slice(0, params.cap);
}
async function readExistingBeliefs(filePath) {
	const content = await fs.readFile(filePath, "utf-8").catch(() => "");
	if (!content) return [];
	const out = [];
	for (const rawLine of content.split("\n")) {
		const line = rawLine.trim();
		if (!line.startsWith("- ")) continue;
		const stripped = line.replace(/^-\s+/, "").replace(/<!--[\s\S]*?-->/g, "").trim();
		const confidenceMatch = stripped.match(/\(confidence:\s*(\d+)\)\s*$/);
		const confidence = confidenceMatch?.[1] ? Number.parseInt(confidenceMatch[1], 10) : 1;
		const text = stripped.replace(/\s*\(confidence:\s*\d+\)\s*$/, "").trim();
		if (!text) continue;
		out.push({
			text,
			confidence,
			hash: hashNormalizedText(text)
		});
	}
	return out;
}
function buildSymiBeliefsMd(params) {
	const stamp = new Date(params.now).toISOString();
	const lines = [];
	lines.push("# Symi Beliefs");
	lines.push("");
	lines.push(`<!-- generated by L3 consolidation on ${stamp}; do not hand-edit -->`);
	lines.push("<!-- each entry: synthesized belief + (confidence = times reinforced across sessions) -->");
	lines.push("");
	if (params.beliefs.length === 0) {
		lines.push("_No beliefs synthesized yet. Beliefs emerge when clusters of consistent observations accumulate._");
		lines.push("");
		return lines.join("\n");
	}
	for (const belief of params.beliefs) {
		const text = belief.text.replace(/\n+/g, " ").trim();
		lines.push(`- ${text} (confidence: ${belief.confidence})`);
	}
	lines.push("");
	return lines.join("\n");
}
function hashNormalizedText(text) {
	const normalized = text.toLowerCase().replace(/\s+/g, " ").trim();
	return crypto.createHash("sha256").update(normalized).digest("hex");
}

//#endregion
export { CLUSTER_MIN_IMPORTANCE, CLUSTER_MIN_SIZE, CLUSTER_MUTUAL_THRESHOLD, DEFAULT_MIN_INTERVAL_MS, KIND_HALF_LIFE_DAYS, KIND_WEIGHT, META_LAST_L3_CYCLE_AT, PROPAGATION_FACTOR, PRUNE_MIN_AGE_DAYS, PRUNE_MIN_IMPORTANCE, REINFORCE_SIMILARITY_THRESHOLD, SYMI_BELIEFS_MAX, SYMI_BELIEFS_PATH, SYMI_CORE_MAX_BULLETS_PER_SECTION, SYMI_CORE_PATH, findSynthesisClusters, runL3Cycle, runL3CycleIfDue, stageDecay, stagePromote, stagePropagate, stagePrune, stageReinforce };