/** * The default config tree. Mirrors `schema.ts` exactly. When merging YAML, * we deep-merge over this tree so users only need to specify what they want * to change. */ import type { ResolvedConfig } from "./schema.js"; const FIXED_VIEWER_PORTS: Readonly> = Object.freeze({ openclaw: 18799, hermes: 18800, }); /** Runtime adapters own these well-known ports even for legacy YAML files. */ export function effectiveViewerPort(agent?: string): number | undefined { return agent ? FIXED_VIEWER_PORTS[agent] : undefined; } export const DEFAULT_CONFIG: ResolvedConfig = { version: 1, viewer: { // Per-agent default lives in `templates/config..yaml`: // - openclaw → 18799 // - hermes → 18800 // The fallback here only matters when neither config file exists // (early bootstrap, tests, etc.). port: 18799, bindHost: "127.0.0.1", openOnFirstTurn: false, }, bridge: { port: 18911, mode: "stdio", }, embedding: { provider: "local", endpoint: "", model: "Xenova/all-MiniLM-L6-v2", apiKey: "", providerIgnore: [], providerOrder: [], openRouter: false, maxInputTokens: 1_024, batchSize: 32, cache: { enabled: true, maxItems: 20_000, }, }, llm: { provider: "", endpoint: "", model: "", temperature: 0, fallbackToHost: true, apiKey: "", timeoutMs: 45_000, maxRetries: 3, providerIgnore: [], providerOrder: [], openRouter: false, maxTokens: 1024, headers: {}, }, l3Llm: { // Empty by default — falls back to the shared `llm` settings. // Operators set this when they want a stronger model for L3 // abstraction. L3 runs off the turn-response path, so a slower // but more reliable model improves world-model quality without // affecting companion latency. provider: "", endpoint: "", model: "", apiKey: "", temperature: 0, timeoutMs: 60_000, providerIgnore: [], providerOrder: [], openRouter: false, maxTokens: 4096, headers: {}, }, skillEvolver: { // Empty by default — falls back to the shared `llm` settings. // Operators set this when they want a stronger model (e.g. // claude-sonnet / gpt-5-thinking) for skill crystallisation. provider: "", endpoint: "", model: "", apiKey: "", temperature: 0, timeoutMs: 60_000, providerIgnore: [], providerOrder: [], openRouter: false, maxTokens: 4096, headers: {}, }, storage: { ftsTokenizer: "trigram", }, algorithm: { lightweightMemory: { enabled: true, }, capture: { maxTextChars: 4_000, maxToolOutputChars: 2_000, embedTraces: true, alphaScoring: true, // OpenClaw's tool messages don't include explicit "reflection" // blocks; without synthesis the alpha scorer sees an empty // reflection and forces α = 0 (see `core/capture/alpha-scorer.ts` // line 97). That makes reflection-weighted backprop degenerate // into pure γ-discount and produces flat V distributions — // L2 association + skill crystallization both starve. Enable // synth by default so even turns without explicit reflections // still contribute useful α values. synthReflections: true, llmConcurrency: 4, // Bound topic-end reflect work so dirty startup recovery cannot replay // a huge historical episode into thousands of paid LLM calls. maxReflectLlmCalls: 128, // Recovered episodes are reconstructed from persisted traces; replay // orphans are usually matching drift, so do not insert duplicate rows. maxRecoveryOrphanInserts: 0, // V7 §3.2 batched variant. With "auto" we issue a single LLM call // per episode for both reflection synth and α scoring as long as // the episode is short enough — this collapses 2N per-step calls // (N synth + N α) into 1 batched call. Long episodes (>12 steps) // automatically fall back to the per-step path so the prompt // never overflows the model's context window. R_human + backprop // remain task-end events handled by `core/reward`, unchanged. batchMode: "auto", batchThreshold: 12, // `reflectionContextMode` controls which extra prompt context blocks // topic-end reflection receives: // - "none": no TASK CONTEXT and no DOWNSTREAM STEP PREVIEW // - "task": inject TASK CONTEXT only // - "downstream": inject DOWNSTREAM STEP PREVIEW only // - "task_downstream": inject both blocks // `longEpisodeReflectMode` controls the fallback used when an episode is // too long for batch scoring: // - "per_step_parallel": keep the current parallel per-step path. Each // step is reflected independently, using only the context blocks // enabled by `reflectionContextMode` that are available without // downstream preview. // - "per_step_downstream": still run per-step work in parallel, but // prebuild a bounded DOWNSTREAM STEP PREVIEW for each step (step+1 // through step+N, capped by `downstreamStepCount`) and inject it when // `reflectionContextMode` includes "downstream". reflectionContextMode: "task_downstream", longEpisodeReflectMode: "per_step_downstream", downstreamStepCount: 3, taskContextMaxChars: 800, downstreamContextMaxChars: 1_200, downstreamPerStepMaxChars: 400, synthOutcomeMaxChars: 600, }, reward: { gamma: 0.9, tauSoftmax: 0.5, decayHalfLifeDays: 30, llmScoring: true, implicitThreshold: 0.2, // 10 minutes was too long for interactive chat — users moved on // to the next task before reward ever fired, so no R_human was // ever computed and V stayed 0 for every trace. 30 s gives the // user a short window to reply ("thanks", "no, try again") that // the scorer picks up as explicit feedback; when nothing // arrives, the implicit fallback fires promptly so downstream // L2/L3/Skill stages aren't starved of signal. feedbackWindowSec: 30, summaryMaxChars: 2_000, llmConcurrency: 2, // Default lowered 2→1 to support single-shot CLI patterns // (`hermes chat -q "..."`, `openclaw run --once`). With the old // floor every CLI single-query episode was abandoned with // "对话轮次不足", starving reward → L2 → Skill of any signal. // Multi-turn TUI flows still trigger reward as before because // they always satisfy the looser bound. Operators wanting the // strict pre-2026Q2 behaviour can set 2 in config.yaml. minExchangesForCompletion: 1, // Lowered 80→40 to match the relaxed exchanges floor. 40 chars // is "ok"/"thanks" + a real follow-up clause; below that we // still skip as a triviality gate. minContentCharsForCompletion: 40, toolHeavyRatio: 0.7, minAssistantCharsForToolHeavy: 80, }, l2Induction: { minSimilarity: 0.65, candidateTtlDays: 30, minEpisodesForInduction: 1, // Lowered from 0.05 → 0.005. Reward backprop V values for typical // multi-step turns (5-15 steps) are clustered around 0.02-0.5 even // for successful episodes; the old 0.05 floor was throwing away // most of the signal before induction could see it. Negative-V // traces are still excluded (they'd never count as "with-set" // evidence anyway). minTraceValue: 0.005, useLlm: true, traceCharCap: 3_000, gainEmaAlpha: 0.4, archiveGain: -0.05, }, l3Abstraction: { // Lowered from 3 → 2. The original threshold required THREE // distinct active policies in the same domain cluster before any // world model could form, which in real usage takes weeks to // accumulate even for a focused user. Two compatible active // policies is the smallest meaningful cluster. minPolicies: 1, // Lowered from 0.1 → 0.02. With the Bayesian-shrinkage gain // formula (see core/memory/l2/gain.ts), a genuinely useful policy // that fires on a single-success path now scores around 0.05-0.20 // (proportional to V_with - 0.5). 0.02 is well below that floor // but still cleanly rejects net-neutral noise. minPolicyGain: 0.02, minPolicySupport: 1, // Lowered from 0.6 → 0.3 so the typical 2-3 active policies in // an early-life install can still cluster into a world model; // strict 0.6 starved L3 in real usage. clusterMinSimilarity: 0.3, maxPoliciesPerCluster: 20, maxPromptChars: 32_000, policyCharCap: 800, traceCharCap: 500, traceEvidencePerPolicy: 1, useLlm: true, // Lowered from 1 → 0 so abstraction can run as soon as the // ingredients show up, not on a per-day cadence. cooldownDays: 0, confidenceDelta: 0.05, minConfidenceForRetrieval: 0.2, }, skill: { // Lowered from 2 → 1: a single supporting episode is enough to // attempt skill crystallization. Quality is still gated by // minGain + candidate trials below. minSupport: 1, // Lowered from 0.1 → 0.02. Same rationale as l3.minPolicyGain: // the new shrinkage-anchored gain formula gives positive scores // proportional to V_with − 0.5, so 0.1 was unreachable for any // policy that didn't have an explicit failure-cohort contrast. // 0.02 is enough to filter neutral-noise policies while still // letting genuinely-useful patterns crystallize. minGain: 0.02, // Lowered from 5 → 1. Demanding multiple trials in `candidate` // before a skill can graduate meant skills rarely promoted in // real usage; 1 lets the candidate→active transition happen // immediately on first successful invocation. candidateTrials: 1, // Verification failures are retried after six hours by default; // operators may set this to 0 when immediate re-evaluation is desired. cooldownMs: 6 * 60 * 60 * 1000, traceCharCap: 500, evidenceLimit: 6, useLlm: true, etaDelta: 0.1, archiveEta: 0.1, minEtaForRetrieval: 0.1, idleArchiveMs: 30 * 24 * 60 * 60 * 1000, }, feedback: { failureThreshold: 3, failureWindow: 5, valueDelta: 0.5, minLowValueThreshold: 0.01, useLlm: true, attachToPolicy: true, cooldownMs: 60_000, traceCharCap: 500, evidenceLimit: 4, }, session: { followUpMode: "merge_follow_ups", mergeMaxGapMs: 2 * 60 * 60 * 1000, maxTurnsPerEpisode: 30, classifyTimeoutMs: 5000, bgLlmConcurrency: 2, }, retrieval: { tier1TopK: 3, tier2TopK: 5, tier3TopK: 2, candidatePoolFactor: 4, weightCosine: 0.6, weightPriority: 0.4, mmrLambda: 0.7, includeLowValue: false, rrfConstant: 60, minSkillEta: 0.1, // Lowered from 0.35 → 0.25 so partial-match traces still surface // for users with smaller corpora. minTraceSim: 0.25, episodeGoalMinSim: 0.45, tagFilter: "auto", keywordTopK: 20, // Lowered from 0.4 → 0.2 with the 2026 ranker overhaul: the new // base relevance already uses channel rank as a first-class // signal, so the old 0.4 floor was over-pruning keyword hits // with modest V·decay. relativeThresholdFloor: 0.2, skillEtaBlend: 0.15, smartSeed: true, smartSeedRatio: 0.7, multiChannelBypass: true, skillInjectionMode: "summary", skillSummaryChars: 200, llmFilterEnabled: true, // Tighter than the legacy default (5) so the LLM filter has a // small budget; combined with the richer prompt (v3) this keeps // packets concise without over-dropping. llmFilterMaxKeep: 4, // Set to 2: skip the LLM precision pass when there's only one // candidate (no point ranking a single item). Anything with 2+ // candidates still goes through the filter to drop off-topic // hits before injection. llmFilterMinCandidates: 2, llmFilterCandidateBodyChars: 500, // Default 0 — no time-window bound, keeping the legacy // brute-force scan behaviour for fresh installs that haven't // grown past the threshold where the bound starts paying off. // Operators with >50K traces are expected to flip this on (we // suggest 86_400_000 = 24h, or 2_592_000_000 = 30 days). vectorScanMaxAgeMs: 0, }, }, hub: { enabled: false, role: "client", port: 18912, address: "", teamName: "", teamToken: "", userToken: "", nickname: "", }, telemetry: { enabled: true }, logging: { level: "info", detailedView: false, timezone: "UTC", console: { enabled: true, pretty: true, channels: ["*"] }, file: { enabled: true, format: "json", rotate: { maxSizeMb: 50, maxFiles: 14, gzip: true }, retentionDays: 30, }, audit: { enabled: true, rotate: { monthly: true, gzip: true }, }, llmLog: { enabled: true, redactPrompts: false, redactCompletions: false }, perfLog: { enabled: true, sampleRate: 1.0 }, eventsLog: { enabled: true }, redact: { extraKeys: ["api_key", "secret", "token", "password", "authorization"], extraPatterns: [], }, channels: {}, }, }; /** * Object-valued config slots whose child keys are user-defined rather than * fields in `DEFAULT_CONFIG`. Keep this list explicit: treating every empty * default object as a free-form map would silently disable unknown-key * warnings for any future structured config section that starts out empty. */ export const FREE_FORM_CONFIG_PATHS: readonly string[] = Object.freeze([ "llm.headers", "l3Llm.headers", "skillEvolver.headers", "logging.channels", ]); /** * Set of dotted-path field names whose values must never be sent to the * viewer or any non-localhost surface. Used by `server/routes/config.ts`. */ export const SECRET_FIELD_PATHS: readonly string[] = Object.freeze([ "embedding.apiKey", "llm.apiKey", "skillEvolver.apiKey", "l3Llm.apiKey", "hub.teamToken", "hub.userToken", ]);