/** * The single schema for `config.yaml`. Used to: * 1. Validate user files at load time (`loadConfig`). * 2. Provide JSON Schema for editor autocomplete (writer can emit it). * 3. Generate the `templates/config..yaml` defaults during code review. * * Adding fields: provide a default in `defaults.ts` (so old configs upgrade). * Removing fields: log a warning at load time; don't crash. */ import { Type, type Static } from "@sinclair/typebox"; // ─── Reusable building blocks ─────────────────────────────────────────────── const StringWithDefault = (def = "") => Type.String({ default: def }); const Bool = (def: boolean) => Type.Boolean({ default: def }); const NumberInRange = (def: number, min?: number, max?: number, description?: string) => Type.Number({ default: def, ...(min != null ? { minimum: min } : {}), ...(max != null ? { maximum: max } : {}), ...(description ? { description } : {}), }); // ─── Sub-schemas ──────────────────────────────────────────────────────────── const ViewerSchema = Type.Object({ port: NumberInRange(18799, 1, 65535), bindHost: StringWithDefault("127.0.0.1"), openOnFirstTurn: Bool(false), }, { default: {} }); const BridgeSchema = Type.Object({ port: NumberInRange(18911, 1, 65535), mode: Type.Union([Type.Literal("stdio"), Type.Literal("tcp")], { default: "stdio" }), }, { default: {} }); const EmbeddingSchema = Type.Object({ provider: Type.Union([ Type.Literal("local"), Type.Literal("openai_compatible"), Type.Literal("gemini"), ], { default: "local" }), endpoint: StringWithDefault(""), model: StringWithDefault("Xenova/all-MiniLM-L6-v2"), apiKey: StringWithDefault(""), /** OpenRouter provider routing — providers to skip. */ providerIgnore: Type.Optional(Type.Array(Type.String(), { default: [] })), /** OpenRouter provider routing — preferred order. */ providerOrder: Type.Optional(Type.Array(Type.String(), { default: [] })), /** Explicitly enable OpenRouter fields for a reverse proxy or CNAME. */ openRouter: Type.Optional(Bool(false)), /** * Maximum estimated tokens in one provider input. `0` explicitly disables * client-side chunking. New installations default to a conservative 1024. */ maxInputTokens: NumberInRange( 1_024, 0, 1_000_000, "Maximum estimated tokens per embedding input. Set to 0 to disable client-side chunking.", ), /** Maximum physical texts sent in one embedding-provider HTTP request. */ batchSize: NumberInRange(32, 1, 256), cache: Type.Object({ enabled: Bool(true), maxItems: NumberInRange(20_000, 0), }, { default: {} }), }, { default: {} }); const ReasoningSchema = Type.Object({ /** * OpenRouter-compatible reasoning toggle. Omit the whole block to keep * the provider/model default. */ enabled: Type.Optional(Type.Boolean()), /** Optional provider effort hint for reasoning-capable models. */ effort: Type.Optional(Type.Union([ Type.Literal("minimal"), Type.Literal("none"), Type.Literal("low"), Type.Literal("medium"), Type.Literal("high"), Type.Literal("xhigh"), Type.Literal("max"), ])), /** Optional token budget for reasoning-capable providers. */ maxTokens: Type.Optional(Type.Number({ minimum: 1 })), }, { default: {} }); const LlmSchema = Type.Object({ provider: Type.Union([ Type.Literal(""), Type.Literal("local_only"), Type.Literal("openai_compatible"), Type.Literal("gemini"), Type.Literal("anthropic"), Type.Literal("bedrock"), Type.Literal("host"), ], { default: "" }), endpoint: StringWithDefault(""), model: StringWithDefault(""), temperature: NumberInRange(0, 0, 2), /** When true, fall back to the agent host's LLM if `provider` fails. */ fallbackToHost: Bool(true), apiKey: StringWithDefault(""), /** Per-call timeout in ms. */ timeoutMs: NumberInRange(45_000, 1_000), /** Max retries on transient errors. */ maxRetries: NumberInRange(3, 0, 10), /** OpenRouter provider routing — providers to skip. */ providerIgnore: Type.Optional(Type.Array(Type.String(), { default: [] })), /** OpenRouter provider routing — preferred order. */ providerOrder: Type.Optional(Type.Array(Type.String(), { default: [] })), /** Explicitly enable OpenRouter fields for a reverse proxy or CNAME. */ openRouter: Type.Optional(Bool(false)), /** Optional reasoning control (see ReasoningSchema). Omit = model default. */ reasoning: Type.Optional(ReasoningSchema), /** Max output tokens per completion (deepseek-v4-flash needs >= 100). */ maxTokens: NumberInRange(1024, 100, 131072), /** Extra HTTP headers for the provider request. */ headers: Type.Optional(Type.Record(Type.String(), Type.String(), { default: {} })), }, { default: {} }); /** * Dedicated model slot for the **skill evolver** (V7 Phase 11 skill * crystallisation + Phase 10 L2 induction). Often the operator wants * a more capable model here than for the per-turn summarizer because * skill generation writes code that will be invoked by the agent. * * All fields are optional. When `model` is empty we fall back to the * main `llm.*` settings — this matches the legacy plugin's "使用 * Summarizer" button and keeps fresh installs zero-config. */ const SkillEvolverSchema = Type.Object({ provider: Type.Union([ Type.Literal(""), Type.Literal("openai_compatible"), Type.Literal("gemini"), Type.Literal("anthropic"), ], { default: "" }), endpoint: StringWithDefault(""), model: StringWithDefault(""), apiKey: StringWithDefault(""), temperature: NumberInRange(0, 0, 2), timeoutMs: NumberInRange(60_000, 1_000), /** OpenRouter provider routing — providers to skip. */ providerIgnore: Type.Optional(Type.Array(Type.String(), { default: [] })), /** OpenRouter provider routing — preferred order. */ providerOrder: Type.Optional(Type.Array(Type.String(), { default: [] })), /** Explicitly enable OpenRouter fields for a reverse proxy or CNAME. */ openRouter: Type.Optional(Bool(false)), /** Optional reasoning control (see ReasoningSchema). Omit = model default. */ reasoning: Type.Optional(ReasoningSchema), /** Max output tokens per completion. */ maxTokens: NumberInRange(1024, 100, 131072), /** Extra HTTP headers for the provider request. */ headers: Type.Optional(Type.Record(Type.String(), Type.String(), { default: {} })), }, { default: {} }); const StorageSchema = Type.Object({ /** * Keyword tokenizer mode used when compiling FTS5 MATCH expressions. * `trigram` preserves the historical SQLite trigram behavior; `cjk` * keeps short Chinese words and mixed ASCII+CJK tokens searchable. */ ftsTokenizer: Type.Union([ Type.Literal("trigram"), Type.Literal("cjk"), ], { default: "trigram" }), }, { default: {} }); const AlgorithmSchema = Type.Object({ lightweightMemory: Type.Object({ /** * Low-cost mode for users who only want raw conversation memory + * recall. When enabled, the runtime skips task/reward/L2/L3/skill * evolution and keeps only summarize + embedding + retrieval filter. * The viewer exposes the inverse as "memory self-evolution". */ enabled: Bool(true), }, { default: {} }), capture: Type.Object({ /** Cap on agent/user text length (chars). Longer content is summarized. */ maxTextChars: NumberInRange(4_000, 200, 64_000), /** Maximum tool outputs we keep verbatim per step. Extras are truncated. */ maxToolOutputChars: NumberInRange(2_000, 200, 32_000), /** Embed state+action vectors when writing traces. Default on. */ embedTraces: Bool(true), /** When true, ask the LLM to score α for each reflection. Default on. */ alphaScoring: Bool(true), /** Synthesize reflections with the LLM if extractor found none. Default off. */ synthReflections: Bool(false), /** Concurrency for α scoring + synth LLM calls (per_step mode only). */ llmConcurrency: NumberInRange(4, 1, 32), /** Hard cap for one topic-end reflect pass, including recovery replay. */ maxReflectLlmCalls: NumberInRange(128, 0, 10_000), /** Max orphan trace inserts allowed during startup-recovered replay. */ maxRecoveryOrphanInserts: NumberInRange(0, 0, 10_000), /** * V7 §3.2 batched variant. When/how to fold per-step reflection synth + * α scoring into one episode-level LLM call: * - "per_step" : legacy path, N per-step LLM calls * - "per_episode" : always batch * - "auto" : batch when stepCount ≤ batchThreshold, else per-step */ batchMode: Type.Union( [Type.Literal("per_step"), Type.Literal("per_episode"), Type.Literal("auto")], { default: "auto" }, ), /** * Step-count cap for "auto" mode. Episodes above this limit fall back * to per-step calls so the batched prompt cannot overflow context. */ batchThreshold: NumberInRange(12, 1, 64), /** * Optional context blocks for per-step reflection and α prompts. * Defaults to "task" to preserve the current task-summary enrichment; * downstream preview remains opt-in. */ reflectionContextMode: Type.Union( [ Type.Literal("none"), Type.Literal("task"), Type.Literal("downstream"), Type.Literal("task_downstream"), ], { default: "task" }, ), /** * Long-episode fallback mode after batch auto-threshold is exceeded. * `per_step_downstream` keeps parallelism but adds step+1..step+3 preview. */ longEpisodeReflectMode: Type.Union( [Type.Literal("per_step_parallel"), Type.Literal("per_step_downstream")], { default: "per_step_parallel" }, ), /** Max downstream steps attached to a per-step prompt. */ downstreamStepCount: NumberInRange(3, 0, 3), /** Character cap for the task-context block. */ taskContextMaxChars: NumberInRange(800, 100, 4_000), /** Total character cap for all downstream preview blocks. */ downstreamContextMaxChars: NumberInRange(1_200, 0, 8_000), /** Character cap per downstream preview block. */ downstreamPerStepMaxChars: NumberInRange(400, 100, 2_000), /** Character cap for current-step tool outcome in synth / α prompts. */ synthOutcomeMaxChars: NumberInRange(600, 100, 4_000), }, { default: {} }), reward: Type.Object({ /** V7 §0.6 eq. 4/5: discount factor γ for reflection-weighted backprop. */ gamma: NumberInRange(0.9, 0, 1), /** V7 §2.4.5 eq. 3: temperature τ for softmax reweighting in L2 induction. */ tauSoftmax: NumberInRange(0.5, 0.01, 10), /** V7 §3.3: priority decay half-life in days. */ decayHalfLifeDays: NumberInRange(30, 1, 365), /** Ask LLM to score user feedback → R_human. Off falls back to polarity heuristics. */ llmScoring: Bool(true), /** Auto-trigger backprop when R_human ≥ this from implicit signals. */ implicitThreshold: NumberInRange(0.2, 0, 1), /** * Seconds to wait for explicit user feedback after `capture.done` before * falling back to implicit-signals scoring. 0 disables the timer. */ feedbackWindowSec: NumberInRange(600, 0, 86_400), /** Max characters for the task summary fed into the human-scorer LLM. */ summaryMaxChars: NumberInRange(2_000, 200, 16_000), /** Concurrency for human-scoring LLM calls. */ llmConcurrency: NumberInRange(2, 1, 16), /** * Min user↔assistant *exchanges* before an episode is scored. * Shorter episodes are closed as abandoned. Default 1 — admits * single-shot CLI patterns (`hermes chat -q "..."`, * `openclaw run --once`) which always have exactly one * user-assistant pair. Set 2 for the strict legacy behaviour * (skip episodes that aren't a real back-and-forth). */ minExchangesForCompletion: NumberInRange(1, 1, 20), /** * Min combined user+assistant content characters before scoring. * Filters trivial turns ("hi"/"ok"). Default 40 — pairs with the * relaxed exchanges floor; raise to 80+ if your workflow always * sends long prompts and you want stronger triviality gating. */ minContentCharsForCompletion: NumberInRange(40, 0, 4_000), /** * Fraction of turns that are tool calls above which an episode is * considered "tool-heavy". When combined with low assistant text * the episode is skipped as noise. Default 0.7 (70%). */ toolHeavyRatio: NumberInRange(0.7, 0, 1), /** * Minimum total assistant content chars to keep an episode that * would otherwise be flagged by the tool-heavy heuristic. If the * assistant wrote at least this many characters the episode is * scored normally even if tool calls dominate. Default 80. */ minAssistantCharsForToolHeavy: NumberInRange(80, 0, 10_000), }, { default: {} }), l2Induction: Type.Object({ /** Cosine ≥ this to associate a new trace with an existing L2 policy. */ minSimilarity: NumberInRange(0.72, 0, 1), /** TTL (days) for unpromoted rows in `l2_candidate_pool`. */ candidateTtlDays: NumberInRange(30, 1), /** Min distinct episodes in a candidate bucket before we run induction. */ minEpisodesForInduction: NumberInRange(1, 1, 20), /** Ignore traces whose V is below this floor (prevents noise-driven L2). */ minTraceValue: NumberInRange(0.01, -1, 1), /** When true, call the LLM to induce policies; else collect candidates only. */ useLlm: Bool(true), /** Character cap for traces handed into the `l2.induction` prompt. */ traceCharCap: NumberInRange(3_000, 600, 16_000), /** EMA alpha for gain updates. 1 means overwrite, lower values preserve history. */ gainEmaAlpha: NumberInRange(0.4, 0, 1), /** Archive active policies whose gain dips below this value. */ archiveGain: NumberInRange(-0.05, -1, 1), }, { default: {} }), l3Abstraction: Type.Object({ /** Minimum number of compatible active L2 policies to trigger an L3 abstraction. */ minPolicies: NumberInRange(1, 1, 50), /** Hard minimum gain for an L2 to be eligible as abstraction evidence. */ minPolicyGain: NumberInRange(0.02, -1, 1), /** Hard minimum support for an L2 to be eligible as abstraction evidence. */ minPolicySupport: NumberInRange(1, 1), /** * Cosine ≥ this between two L2 vectors → same bucket. Buckets below this * are ignored (policies too disparate to share a world model). */ clusterMinSimilarity: NumberInRange(0.6, 0, 1), /** Maximum policies included in one L3 abstraction prompt. */ maxPoliciesPerCluster: NumberInRange(20, 1, 100), /** Hard total character cap for one L3 abstraction prompt. */ maxPromptChars: NumberInRange(32_000, 4_000, 128_000), /** Chars of L2 body handed to `l3.abstraction`. */ policyCharCap: NumberInRange(800, 200, 4_000), /** Chars of trace body handed per evidence trace. */ traceCharCap: NumberInRange(500, 100, 4_000), /** Max evidence traces in the prompt — one per policy. */ traceEvidencePerPolicy: NumberInRange(1, 0, 4), /** * When true, call `l3.abstraction` to generate/update world models. * When false, buckets are logged but no LLM call fires — useful for * cost-sensitive deployments. */ useLlm: Bool(true), /** Cooldown in days between L3 runs for the same domain tag. */ cooldownDays: NumberInRange(1, 0, 365), /** Confidence delta per positive/negative user feedback. */ confidenceDelta: NumberInRange(0.05, 0, 1), /** Below this confidence, a world model is hidden from Tier-3 retrieval. */ minConfidenceForRetrieval: NumberInRange(0.2, 0, 1), }, { default: {} }), skill: Type.Object({ minSupport: NumberInRange(2, 1), // V7 §2.5 graduation floor. The schema allows negative values so // demo / single-success-line scenarios (where with-without ≈ 0 by // construction even after Bayesian shrinkage) can still force- // graduate candidate policies into active. Production default is // 0.02 — see `core/config/defaults.ts` for rationale and // `core/memory/l2/gain.ts` for how gain is now anchored to a // neutral 0.5 baseline so this floor is reachable on real data. minGain: NumberInRange(0.02, -1, 1), /** Trials a skill must accumulate in `candidate` before it can graduate. */ candidateTrials: NumberInRange(3, 1), /** Back-off before we retry a failed-to-verify policy. */ cooldownMs: NumberInRange(6 * 60 * 60 * 1000, 0, 30 * 24 * 60 * 60 * 1000), /** Chars per evidence trace fed into the crystallize prompt. */ traceCharCap: NumberInRange(500, 100, 4_000), /** Max evidence traces per policy given to the LLM. */ evidenceLimit: NumberInRange(6, 1, 20), /** Turn the LLM crystallization off (collect candidates only). */ useLlm: Bool(true), /** η delta applied per user thumbs up/down. */ etaDelta: NumberInRange(0.1, 0, 1), /** Archive an active skill whose η drops below this. */ archiveEta: NumberInRange(0.1, 0, 1), /** Hide Tier-1 skills whose η is below this. Mirrors retrieval.minSkillEta. */ minEtaForRetrieval: NumberInRange(0.1, 0, 1), /** Archive low-η active skills after this much retrieval inactivity (minimum 1 hour). */ idleArchiveMs: NumberInRange( 30 * 24 * 60 * 60 * 1000, 60 * 60 * 1000, 365 * 24 * 60 * 60 * 1000, ), }, { default: {} }), feedback: Type.Object({ /** Raise a burst after this many failures of the same tool in-window. */ failureThreshold: NumberInRange(3, 2, 20), /** Rolling window (number of steps) for the burst counter. */ failureWindow: NumberInRange(5, 2, 50), /** Min |mean(high) - mean(low)| to fire without an explicit user signal. */ valueDelta: NumberInRange(0.5, 0, 2), /** * Minimum absolute value threshold for lowValue traces. Only traces with * value < -minLowValueThreshold will be collected as failure evidence * (unless they match isFailureLike patterns). This filters out trivial * negative feedback (e.g., value = -0.001) and focuses on genuine failures. * Default 0.01 — adjust higher (e.g., 0.1) to be more conservative. */ minLowValueThreshold: NumberInRange(0.01, 0, 1), /** Let the LLM rewrite the preference / anti-pattern lines. */ useLlm: Bool(true), /** Tag the L2 policies referenced by the evidence with the guidance. */ attachToPolicy: Bool(true), /** Debounce (ms) for repeat repairs on the same context hash. */ cooldownMs: NumberInRange(60_000, 0, 24 * 60 * 60 * 1000), /** Char cap per trace handed to the repair prompt. */ traceCharCap: NumberInRange(500, 100, 4_000), /** Max evidence traces per class (high-value / low-value). */ evidenceLimit: NumberInRange(4, 1, 20), }, { default: {} }), session: Type.Object({ /** * How a user's next message should relate to the previously closed * episode. Mirrors V7 §0.1 but softens the default so same-topic * follow-ups stay in one "task" from the user's POV. * * - "merge_follow_ups" (default) — both `revision` and `follow_up` * reopen the previous episode and append the new turn. Only * `new_task` opens a fresh episode/session. This matches the * legacy `memos-local-openclaw` behaviour where one "task" * aggregates many related turns and skills crystallise from a * coherent transcript. * - "episode_per_turn" — follow-ups open a NEW episode in * the same session (V7 §0.1 strict). Each user query gets its * own R_human + V backprop pass. Useful when you want fine-grained * credit assignment per sub-task. */ followUpMode: Type.Union([ Type.Literal("merge_follow_ups"), Type.Literal("episode_per_turn"), ], { default: "merge_follow_ups" }), /** * Hard cap on how long a single merged episode can grow before we * force a new episode boundary even if relation says "follow_up". * Prevents infinite growth and keeps reward scoring tractable. * 0 disables the cap. Default: 2 hours — matches the legacy * `taskIdleTimeoutMs`. */ mergeMaxGapMs: NumberInRange(2 * 60 * 60 * 1000, 0, 24 * 60 * 60 * 1000), /** * Hard cap on turns in a merged episode. Once reached, the next * turn forces a topic boundary even if relation classification says * follow-up/revision. Keeps task-end processing bounded. */ maxTurnsPerEpisode: NumberInRange(30, 5, 200), /** * Max time to wait for relation classification before defaulting * to a conservative new-task boundary so foreground prompt * construction cannot stall indefinitely. */ classifyTimeoutMs: NumberInRange(5000, 1000, 30000), /** * Shared LLM concurrency budget for asynchronous background * capture/reward/L2/L3/skill-evolution processing. */ bgLlmConcurrency: NumberInRange(2, 1, 8), }, { default: {} }), retrieval: Type.Object({ /** How many Skill snippets to inject at turn start. */ tier1TopK: NumberInRange(3, 0, 100), /** How many trace/episode snippets to inject. */ tier2TopK: NumberInRange(5, 0, 100), /** How many world-model snippets to inject. */ tier3TopK: NumberInRange(2, 0, 100), /** Fetch K·factor candidates from SQLite before MMR/priority re-rank. */ candidatePoolFactor: NumberInRange(4, 1, 50), /** Tier 2 fusion weight for cosine similarity (vs. priority). */ weightCosine: NumberInRange(0.6, 0, 1), /** Tier 2 fusion weight for max(V,0)·decay(Δt) priority. */ weightPriority: NumberInRange(0.4, 0, 1), /** MMR λ — 1 = pure relevance, 0 = pure diversity. */ mmrLambda: NumberInRange(0.7, 0, 1), /** Hide V<0 traces by default (Decision Repair can override). */ includeLowValue: Bool(false), /** Classic Reciprocal Rank Fusion constant. */ rrfConstant: NumberInRange(60, 1, 10_000), /** Skip Tier-1 skills whose η is below this floor. */ minSkillEta: NumberInRange(0.1, 0, 1), /** Drop Tier-2 hits whose cosine is below this floor. */ minTraceSim: NumberInRange(0.35, 0, 1), /** * V7 §2.6 Tier 2b — minimum goal-level cosine for "episode replay" * to fire. Below this, we don't rollup episodes into a reference * action sequence (individual trace hits still go through). */ episodeGoalMinSim: NumberInRange(0.45, 0, 1), /** auto | off | strict — controls tag-based pre-filtering. */ tagFilter: Type.Union([ Type.Literal("auto"), Type.Literal("off"), Type.Literal("strict"), ], { default: "auto" }), /** * Per-tier keyword (FTS5 + pattern) channel size. Each tier issues * a vector channel + an FTS channel + a pattern channel; this is * the K for the keyword channels (vector still uses * `tier{1,2,3}TopK · candidatePoolFactor`). */ keywordTopK: NumberInRange(20, 0, 200), /** * Drop ranked candidates whose blended `relevance` is below * `topRelevance * relativeThresholdFloor`. Adaptive cousin of * `minTraceSim` — when the best hit is weak, we keep more (lower * absolute floor); when there's a clear winner, we drop noise. * Set to 0 to disable the relative cutoff entirely. * * Default lowered to 0.2 with the 2026 ranker overhaul: the new * base formula already weighs channel-rank evidence (so a raw * FTS-only hit lands in a comparable range to a cosine-0.8 hit), * and the old 0.4 floor was over-pruning keyword matches with * modest V·decay. */ relativeThresholdFloor: NumberInRange(0.2, 0, 1), /** * Tier-1 skill relevance blend weight for `η` (skill reliability). * Old default `0.4` made well-trodden skills outrank obviously-more- * relevant new ones. `0.15` keeps the η nudge but lets the query↔skill * cosine dominate. */ skillEtaBlend: NumberInRange(0.15, 0, 1), /** * MMR Phase-A seed-by-tier policy. When `true`, only seed a tier * if its best candidate's relevance ≥ `poolTopRelevance * * smartSeedRatio` (see below). This prevents the ranker from * force-injecting a stale Tier-1 skill / Tier-3 world-model just * because it cleared the absolute floors. */ smartSeed: Bool(true), /** * Seed cutoff for smart-seed MMR — tier is seeded iff its best * candidate's relevance ≥ `poolTopRelevance * smartSeedRatio`. * Independent of `relativeThresholdFloor` so the seed gate can be * stricter than the generic drop floor (0.7 is "within 30% of the * best available candidate anywhere in the pool"). */ smartSeedRatio: NumberInRange(0.7, 0, 1), /** * When a candidate is surfaced by ≥ 2 retrieval channels (e.g. * both vec and fts hit the same trace), bypass the relative * threshold. Multi-channel agreement is a strong signal, and * without this keyword-only matches with modest V·decay often * get dropped by a noisy `topRelevance`. */ multiChannelBypass: Bool(true), /** * How Tier-1 skills are surfaced in the injected prompt: * - "summary" (default): inject only `name + η + 1-line summary + * a `memos_skill_get(id="…")` hint`. The agent decides whether to * fetch the full procedure via the `memos_skill_get` tool. Keeps the * prompt small and avoids paying for skills the agent never * uses. * - "full": inline the entire `invocationGuide` body (legacy * behaviour — useful for hosts that don't support tool calls). */ skillInjectionMode: Type.Union( [Type.Literal("summary"), Type.Literal("full")], { default: "summary" }, ), /** * Char cap for the per-skill summary body when `skillInjectionMode` * is `summary`. We trim the first paragraph of `invocationGuide` * and clamp to this many chars before appending the call-hint. */ skillSummaryChars: NumberInRange(200, 40, 800), /** * LLM-based relevance filter (`core/retrieval/llm-filter.ts`). * Default on because cosine retrieval over-matches and a single * small LLM call dramatically cuts down irrelevant injections. */ llmFilterEnabled: Bool(true), /** Keep at most this many candidates after the LLM filter. */ llmFilterMaxKeep: NumberInRange(5, 1, 30), /** * Skip the filter when the ranked list has fewer than this many * items. Default 1 — even a single candidate gets a precision * pass, matching `memos-local-openclaw`'s tool-level filter and * preventing a lone off-topic memory from sneaking through * unchecked. */ llmFilterMinCandidates: NumberInRange(1, 1, 50), /** * Body-text budget per candidate when building the LLM filter * prompt. Higher = more context for precise judgement, at the * cost of more tokens per round-trip. Default 500 (openclaw uses * 300 without tags/channels; we include richer metadata, so a * slightly larger window pays for itself). */ llmFilterCandidateBodyChars: NumberInRange(500, 120, 2000), /** * Tier-2 vector scan time-window bound (ms). When > 0, the * vector scan path (`scanAndTopK` in `core/storage/vector.ts`) * only considers traces written within the last * `vectorScanMaxAgeMs` milliseconds. Set to `0` to disable the * cap (legacy behaviour: full-table brute-force scan). * * Background: at 93K rows × 1536 dims the unbounded scan blocks * the Node event loop for 5–30 s every `onTurnStart` * (https://github.com/MemTensor/MemOS/issues/1929). A 24-hour * window keeps onTurnStart latency under control without * sacrificing recall for active-session memories. FTS keyword * channels still cover older traces, so this bound only affects * the cosine-only path. * * Hard cap is one year (31_536_000_000 ms) — anything larger is * indistinguishable from "unbounded" at the corpus sizes where * the bound starts to matter, and accepting absurdly large * values lets misconfigured deployments silently revert to the * old behaviour. */ vectorScanMaxAgeMs: NumberInRange(0, 0, 31_536_000_000), }, { default: {} }), }, { default: {} }); const HubSchema = Type.Object({ enabled: Bool(false), role: Type.Union([Type.Literal("hub"), Type.Literal("client")], { default: "client" }), port: NumberInRange(18912, 1, 65535), address: StringWithDefault(""), teamName: StringWithDefault(""), teamToken: StringWithDefault(""), userToken: StringWithDefault(""), nickname: StringWithDefault(""), }, { default: {} }); const TelemetrySchema = Type.Object({ enabled: Bool(true), }, { default: {} }); const LoggingSchema = Type.Object({ level: Type.Union([ Type.Literal("trace"), Type.Literal("debug"), Type.Literal("info"), Type.Literal("warn"), Type.Literal("error"), Type.Literal("fatal"), ], { default: "info" }), /** Viewer-only switch: expose detailed logs, lifecycle tags and chain view. */ detailedView: Bool(false), /** IANA timezone for log timestamp display. */ timezone: StringWithDefault("UTC"), console: Type.Object({ enabled: Bool(true), pretty: Bool(true), channels: Type.Array(Type.String(), { default: ["*"] }), }, { default: {} }), file: Type.Object({ enabled: Bool(true), format: Type.Union([Type.Literal("json"), Type.Literal("compact")], { default: "json" }), rotate: Type.Object({ maxSizeMb: NumberInRange(50, 1), maxFiles: NumberInRange(14, 1), gzip: Bool(true), }, { default: {} }), /** Days to keep regular app/error/perf/llm/events files. */ retentionDays: NumberInRange(30, 1), }, { default: {} }), audit: Type.Object({ enabled: Bool(true), /** Audit retention is "forever": rotate by month, gzip; never delete. */ rotate: Type.Object({ monthly: Bool(true), gzip: Bool(true), }, { default: {} }), }, { default: {} }), llmLog: Type.Object({ enabled: Bool(true), redactPrompts: Bool(false), redactCompletions: Bool(false), }, { default: {} }), perfLog: Type.Object({ enabled: Bool(true), sampleRate: NumberInRange(1.0, 0, 1), }, { default: {} }), eventsLog: Type.Object({ enabled: Bool(true), }, { default: {} }), redact: Type.Object({ extraKeys: Type.Array(Type.String(), { default: ["api_key", "secret", "token", "password", "authorization"] }), extraPatterns: Type.Array(Type.String(), { default: [] }), }, { default: {} }), /** Per-channel level overrides, e.g. `{ "core.l2.cross-task": "debug" }`. */ channels: Type.Record(Type.String(), Type.String(), { default: {} }), }, { default: {} }); // ─── Top-level schema ─────────────────────────────────────────────────────── export const ConfigSchema = Type.Object({ version: NumberInRange(1, 1), viewer: ViewerSchema, bridge: BridgeSchema, embedding: EmbeddingSchema, llm: LlmSchema, /** Dedicated model slot for L3 abstraction. Same shape as skillEvolver. */ l3Llm: SkillEvolverSchema, skillEvolver: SkillEvolverSchema, storage: StorageSchema, algorithm: AlgorithmSchema, hub: HubSchema, telemetry: TelemetrySchema, logging: LoggingSchema, }, { default: {} }); export type ReasoningConfig = Static; export type ResolvedConfig = Static;