Learning Core
Learning Core is the bundled extension that turns finished chats into lessons and queued skill proposals. This page covers operation: enabling it, tuning promotion thresholds, placing its storage, and verifying a deployment. For what end users see, see Learning; for internals and extension seams, see Learning Core internals.Enable the plugin
The plugin is disabled by default. Enable the entry and set the mode:systemctl --user restart openclaw-gateway; macOS: wednesdayai gateway restart --deep).
mode accepts off, observe (default), and assist:
offstops capture entirely.observerecords candidates and promotes lessons but never changes replies.assistdoes the same capture work and injects up tomaxLessonsForPrompt(default5) active lessons into the next reply’s context.
Configuration reference
All keys live underplugins.entries.learning-core.config in openclaw.json:
Invalid values fall back to the default (for example a
minOccurrences of 0 or a non-number resolves to 3), so a typo degrades rather than fails startup.
maxLessonsForPrompt only matters in assist mode. Consolidation and distill stay off unless you set their enabled flags:
Storage placement
Learning Core carries no database of its own. It resolves placement through the shared domain-storage resolver, which follows the system session store by default: a host whose sessions live in Postgres gets learning state in Postgres too, with no extra configuration. Resolved defaults, with<state> being the state directory (~/.openclaw unless OPENCLAW_STATE_DIR is set):
Overriding placement
Setplugins.entries.learning-core.config.storage to pin a backend independently of the system store:
- An explicit
backendwins outright. - With no
backend, adatabaseUrlimpliespostgresand asqlitePathimpliessqlite. - With neither, the system session store backend is used.
schema applies to Postgres only and must match ^[a-z_][a-z0-9_]{0,62}$. It is validated before it reaches the database, because Postgres cannot parameterise an identifier - an unsafe value is rejected at resolution, not interpolated.
Guards
- An unrecognised
backendthrows at resolution rather than silently degrading. - Resolving to
postgreswith nodatabaseUrlon either the override or the system store throws, naming the domain. - A
sqlitePaththat resolves to the same file as core’s own session database is refused, so a learning schema change can never affect session storage.
learning_records, keyed (workspace_id, agent_id, record_type, record_id)) with CREATE TABLE IF NOT EXISTS on first use - there is no separate migration step to schedule.
Capture, promote, and apply
The runtime pipeline has three stages:- Capture. A
session.endclaim admits one redacted candidate when the transcript has a user excerpt, then acknowledgesprocessed. No excerpt means no candidate row; the claim is still acknowledged. The claim path never writes a lesson. - Promote. The
learning-core.promotetask pass (deterministic) turns pending candidates into lessons. The budgeted LLM passlearning-core.consolidationdoes the same under its daily call ceiling. When Dream Cycle is off andconsolidation.enabledis false, promote runs inline right after the claim ack, so observe-mode hosts still get lessons without a night-time job. - Apply.
learning-core.distilland promotion thresholds queue skill proposals. Approval writes atomically (tmp file +fsync+ rename) to$OPENCLAW_STATE_DIR/skills/<name>/SKILL.md(or~/.openclaw/skills/<name>/SKILL.md).learning-core.reconcile-skillsrestores a deleted or mutated skill file from the proposal’s storeddesiredStatehash.
task_runs and logs. Proposals are security-scanned before apply; a proposal that trips a scanner rule is stored as quarantined and cannot transition back to pending.
Capability levels
learning.overview reports a capability alongside the resolved backend:
Backend failure behaviour
Learning Core degrades tofs-jsonl rather than dropping a lesson:
- SQLite that cannot be opened (unwritable path, corrupt file) falls back at construction with a warning logged. The parent directory is created if missing.
- Postgres does not connect at construction, so a refusing server surfaces on first operation. The first rejection logs a warning, swaps to the fs-jsonl store, and retries that one operation there, so the caller still succeeds. After the swap, further errors are the fallback’s own and propagate normally.
fs-jsonl from learning.overview, the fallback has fired - check the gateway log for lesson store backend failed; falling back to fs-jsonl.
Migrating from a JSONL deployment
No migration command is required. When a SQL backend is configured on a host that previously ranfs-jsonl, the store folds the existing log in on read:
- The read-through only reads - it never locks, rewrites, compacts, or deletes the old log.
- Where a lesson id exists in both places, the SQL row wins (it is the newer state).
- It covers lessons only - a proposal that exists solely in the legacy log does not appear in the queue.
learning.overview reports the lesson counts you expect, archive the JSONL directory at your leisure. The read-through resolves it relative to the state directory, so a changed OPENCLAW_STATE_DIR hides the old log without deleting it.
Verifying a deployment
Do not infer capture from a green test suite - observe the live system. None of these scripts callwednesdayai gateway start.
OPENCLAW_VERIFY_SESSION_ID fails closed (exit 2), and a skipped leg is not a pass. Placement-only (never touches live config): bash scripts/verify-learning-core-placement.sh.
Observability
Read-only gateway methods:learning.overview (backend, capability, counts), learning.listLessons, learning.searchLessons (optional k, default 5), learning.listProposals, learning.inspectProposal. Mutating: learning.decideProposal and bulk learning.decideProposals (stops on the first failure).
Claims live in session_consumer_claims under a namespaced consumer id of the form <agentId>:learning-core:session.end - query with a prefix match, not equality against the bare plugin id. The active JSONL log uses the extensions.learningCore rotation profile (maxFileBytes, maxBackups, compress, maxQueuedBytes apply; file is ignored).
Troubleshooting
Claims are processed but no lessons appear. Expected on the claim path. Confirm arecord_type='candidate' row exists for the session with verify-learning-core-full.sh; a lesson appears only after promote (inline when dreams and consolidation are off) or a consolidation pass. If even the candidate is missing, the transcript yielded no user excerpt.
learning.overview reports a backend you did not configure. Either the system session store resolved differently than expected, or a fallback fired - check the gateway log.
Postgres permission errors on first use. The role needs CREATE on the database, or pre-create the plugin_learning_core schema and grant table-creation rights inside it.
PLUGIN_REGISTER_ERROR: Plugin task pass id must not contain "." The plugin failed to load because a pass was registered with a dotted id such as learning-core.promote. Pass ids are bare; the backbone namespaces them. Restart the gateway after deploying the fixed build.
Related
- Learning - what your users see
- Learning Core internals - store contract and extension seams
- Dream Cycle - idle-time reflection that feeds learning candidates
- Configuration - top-level config file basics