#OpenClaw DB-First Session Loading - Feature Request / Help Needed

1 messages · Page 1 of 1 (latest)

barren copper
#

OpenClaw DB-First Session Loading - Feature Request / Help Needed

Background

We're trying to implement DB-first session loading to reduce token usage and improve startup performance.

Current Implementation

  • OpenClaw loads sessions from .jsonl files
  • Large sessions (100+ messages) consume 100k-200k tokens
  • We want to load only recent messages (20-30) from PostgreSQL database

What We've Done

  1. Implemented DB session loader with tiered loading (hot/warm/cold)
  2. Migrated 110 messages to PostgreSQL successfully
  3. Modified source code (src/agents/pi-embedded-runner/run/attempt.ts)
  4. Attempted to compile but encountered issues:
    • A2UI bundle fails
    • Direct modification of dist/*.js files doesn't work reliably

The Problem

Code modifications don't execute:

  • Modified pi-embedded-CCXIVUPP.js and pi-embedded-DF3C75Qw.js
  • Added DB loading logic before SessionManager.open()
  • No logs appear, code path never executes
  • Gateway restarts don't pick up changes

Code Location

// In attempt.ts around line 1096
await prewarmSessionFile(params.sessionFile);

// We added:
if (useDBFirst && params.sessionKey) {
  const dbLoader = getDBSessionLoader();
  const sessionData = await dbLoader.loadSession(params.sessionKey);
  // ... but this never executes
}

sessionManager = guardSessionManager(SessionManager.open(params.sessionFile), {
  // ...
});

Questions

  1. What's the correct way to modify OpenClaw source and rebuild?

    • How to fix A2UI bundle errors?
    • Is there a development build mode?
  2. Is there an official way to implement custom session loaders?

    • Plugin system?
    • Configuration hooks?
  3. Why don't dist file modifications work?

    • Are files cached?
    • Multiple code paths?
warm glacierBOT
#

To help others find answers, you can mark your question as solved via Right click solution message -> Apps -> âś… Mark Solution

barren copper
#

Our Goal

Load only 20-30 recent messages from DB instead of entire .jsonl file:

  • Reduce token usage by 90%+
  • Faster startup
  • Better scalability

Environment

  • OpenClaw: 2026.3.9
  • Node: v22.22.0
  • OS: macOS (Darwin 25.3.0)
  • Database: PostgreSQL 14

Help Needed

  • Guidance on proper compilation workflow
  • Or: official support for DB-first session loading
  • Or: plugin/hook system for custom session loaders

Thank you!

#

OpenClaw DB-First Session Loading - Feature Request / Help Needed

reef ploverBOT
#

A bunch of what you’re trying to achieve is already “official” in OpenClaw, and it’ll save you from fighting the build system.

0) First: make sure you’re editing the code that’s actually running

Before anything else, can you paste:

  • openclaw --version
  • how you’re running the gateway (daemon via launchd/systemd? npx? global install? from a git checkout?)

The “I edited dist and nothing changed” symptom is almost always: you edited a different copy than the one the daemon is executing (global install vs repo checkout vs an npx cache dir), or you edited a generated chunk that isn’t the entrypoint.

1) Correct way to modify source + rebuild (and a dev mode)

For iterating on TS in the gateway, don’t touch dist/*.js manually. Use watch mode:

That runs the gateway under a file watcher and rebuilds/restarts when src/ changes. If your logging still “doesn’t show up” under watch mode, you’re in the wrong codepath (or logging is going somewhere else), not a build-cache issue.

#

For full “packaged” builds from a checkout:

A2UI bundle failures

pnpm build runs A2UI bundling first (pnpm canvas:a2ui:bundle → scripts/bundle-a2ui.sh). That script requires the repo to include:

  • vendor/a2ui/renderers/lit
  • apps/shared/OpenClawKit/Tools/CanvasA2UI

If you’re on Windows, A2UI bundling has known bash/node-path footguns (there’s an active issue trail around this): https://github.com/openclaw/openclaw/issues/26065

If you don’t need Canvas/A2UI for this work, you can usually avoid that whole step by iterating with pnpm gateway:watch (recommended) instead of pnpm build.

2) “DB-first loading” vs official session controls (token reduction)

Important distinction: reading a big .jsonl transcript is I/O; the token burn comes from how much history gets sent to the model.

OpenClaw already has two official mechanisms that target exactly this:

#

If your “100–200k tokens on startup” is basically “first turn after idle re-caches the whole prompt”, session pruning (agents.defaults.contextPruning) is specifically designed to reduce that cache-write spike.

3) Is there an official custom session loader/plugin hook?

There’s a real plugin/hook system, but it’s not “replace SessionManager with your own DB backend” today:

Practical pattern if you want Postgres:

  • keep OpenClaw’s JSONL as the canonical transcript (so everything keeps working),
  • use hooks (or a small sidecar) to mirror transcripts/compaction summaries into Postgres for your own querying/search/analytics,
#
  • tune compaction + pruning to keep the model context small.

If you truly want “SessionManager loads from DB instead of JSONL”, that’s a bigger architectural change and should be a GitHub feature request (with design notes about consistency, compaction semantics, tree/parentId structure, migrations, offline behavior): https://github.com/openclaw/openclaw/issues

4) Why editing dist chunks didn’t work

A few common reasons:

  • you edited a generated hashed chunk (like pi-embedded-XXXX.js) that isn’t the actual entrypoint being imported
  • you edited the repo’s dist/, but the gateway daemon is running the globally installed package (or an npx cache)
  • the gateway spawns child processes; the code you edited might be in a different process/package boundary than you think

If you paste openclaw --version + how you start the gateway, I can tell you exactly which “copy” you need to run (or how to force it to run your checkout).

#

You’re on a recent enough version (2026.3.9) that you probably don’t need “DB-first session loading” to get the 90%+ token reduction.

The “official” solution to “only keep last ~20–30 messages”

OpenClaw already supports persistent compaction (summary + keep recent), and it has explicit knobs for this in the Pi embedded runtime:

So instead of loading only the tail from Postgres, you can configure compaction so the model sees:

  • 1 compaction summary
  • plus ~N tokens of recent conversation

That achieves the same token goal without changing the session storage layer.

If your big token usage is mostly tool output re-entering the prompt, also enable session pruning (TTL-aware tool-result trimming):

#

Why your dist/pi-embedded-XXXX.js edits “don’t run”

Those pi-embedded-<hash>.js files are generated build artifacts. Two common gotchas:

  1. You’re editing a file that isn’t actually the one your running gateway process is loading (macOS app runs its own bundled gateway unless you run your own and attach).
  2. Even if you hit the right install, those hashed chunks get regenerated and/or aren’t the actual executed entrypoint you think they are.

The reliable way is: edit src/** and run the gateway from a checkout in watch mode.

Proper compilation / dev workflow on macOS (skips the A2UI pain)

For backend-only changes like attempt.ts, use gateway watch mode:

This is the key part: run your gateway from the repo, and point the macOS app/UI at it in Local mode if needed. Otherwise you’ll keep restarting the other gateway.

About the A2UI bundle failure

#

pnpm build runs pnpm canvas:a2ui:bundle first (A2UI bundling). If you don’t need Canvas/A2UI for this feature, don’t block on that—iterate with pnpm gateway:watch.

If you do need it, run pnpm canvas:a2ui:bundle directly and paste the exact error. The bundler script requires those repo directories to exist:

  • vendor/a2ui/renderers/lit
  • apps/shared/OpenClawKit/Tools/CanvasA2UI

Is there an official “custom session loader” plugin point?

Not currently for replacing transcript persistence. There is a hooks system (great for mirroring transcripts to Postgres, analytics, etc.), but it won’t swap out SessionManager.open():

If you still want DB-first as a first-class feature, it should be a GitHub issue (it touches consistency, tree/parentId semantics, compaction entries, offline/dev profile behavior): https://github.com/openclaw/openclaw/issues

If you paste:

  • whether you’re running the gateway via the macOS app vs CLI daemon vs from-source
  • the A2UI bundle error output (first ~30 lines + last ~30 lines)
    …I can tell you the fastest path to get your code executing (and confirm you’re editing the right process).
#

You reposted the starter text (no new details), so we’re blocked on one key fact: which gateway binary/process is actually running on your Mac.

On macOS it’s extremely common to be editing a repo checkout (or some dist/ chunk) while the running gateway is the app-bundled one or a global install somewhere else — which makes it look like “my code never executes”.

Fastest way to unstick this (2 things to paste)

  1. Are you running the gateway via OpenClaw.app or via CLI/daemon?
  2. Paste outputs of:
  • which openclaw
  • openclaw gateway status

(If you don’t have openclaw on PATH because you’re running from source only, tell me that.)

If you want a guaranteed “my TS edits run” workflow (no A2UI needed)

From a git checkout of the repo, run watch mode:

#

This is the supported dev loop for changing src/** and seeing it execute.

About the A2UI bundle failing

If you paste the exact error from running:

  • pnpm canvas:a2ui:bundle

…I can tell you whether it’s “missing repo directories” vs “rolldown/tsc tooling” vs a platform/path issue. (But for your attempt.ts work, you can usually ignore A2UI entirely and iterate with pnpm gateway:watch.)

Also: you likely don’t need DB-first to get the token win

Compaction + keepRecentTokens is the supported way to make the model only see a summary + recent context:
https://docs.openclaw.ai/reference/session-management-compaction

Reply with those two command outputs + whether the app is managing your gateway, and we can pinpoint why your edited code path never fires.

barren copper
#

Thanks for the detailed follow-up! Here's the info:

Setup:

openclaw --version: OpenClaw 2026.3.9 (20d097a)
which openclaw: /opt/homebrew/bin/openclaw
Gateway: LaunchAgent daemon (not the app)
Running: /opt/homebrew/lib/node_modules/openclaw/dist/index.js
Why I need DB-first (not just token reduction):
We're running 12+ agents (Aria, Theo, Luna, Kai, Leo, Zara, Iris, Finn, Cleo, Milo, Nova, Jarvis) and the .jsonl approach has scaling issues:

  1. Management: 12+ agents Ă— N sessions = hundreds of .jsonl files scattered across agent dirs
  2. Search/Analytics: Can't query across agents (e.g., "show all sessions about project X")
  3. Monitoring: Hard to track total token usage, session health, etc.
  4. Backup/Migration: Need to sync hundreds of files vs one DB

Our architecture:

  • PostgreSQL (192.168.10.21) already stores: memory, projects, session_messages
  • MCP API (8090) provides unified access
  • We want sessions to be first-class DB entities

Question: Would you consider a feature request for "DB-backed SessionManager" as a plugin/extension point? We're happy to:

  • Maintain compatibility with .jsonl format
  • Contribute the implementation back
  • Document the migration path

Or is there a better pattern for multi-agent deployments?

reef ploverBOT
#

That helps a lot — you’ve confirmed the core reason your source edits / random dist/pi-embedded-<hash>.js edits weren’t behaving:

  • You’re running the Homebrew/global install (/opt/homebrew/lib/node_modules/openclaw/dist/index.js) under a LaunchAgent.
  • Those hashed pi-embedded-XXXX.js files are generated chunks; editing them is brittle, and it’s easy to edit a chunk that isn’t even the one being imported by your running codepath.

If your goal is to contribute a real change: don’t patch the Homebrew dist

For anything non-trivial (like a session backend), the sane workflow is:

  1. run from a git checkout
  2. use watch mode so you know your src/** edits are executing
  3. only when it works, worry about packaging/daemon

Docs for the dev loop:

Also: if you want to keep your real install untouched while testing, use the dev profile (OPENCLAW_PROFILE=dev) so it uses ~/.openclaw-dev instead of your production state. (Same debugging page covers it.)

#

On the architecture question: DB-backed SessionManager as a plugin

I’d frame this as a feature request, yes — but with an important constraint:

OpenClaw’s transcript layer is managed by pi-coding-agent’s SessionManager, and today it fundamentally assumes an append-only JSONL transcript file path (tree structure with id/parentId, compaction entries, etc.). Making it “DB-first” cleanly usually means one of:

  1. Upstream change: introduce a storage abstraction in SessionManager (so OpenClaw can pass JsonlTranscriptBackend vs PostgresTranscriptBackend), or
  2. OpenClaw-side refactor: stop depending on SessionManager.open(filePath) directly and wrap transcript operations behind an interface (bigger change, higher risk), or
  3. Hybrid (often best): keep JSONL as the source-of-truth for the runtime, but mirror/index into Postgres for query/analytics/backup.

The “better pattern” for multi-agent deployments (today)

Given your requirements (cross-agent search/analytics/monitoring/backup), option (3) hybrid is usually the fastest win and least invasive:

  • Keep JSONL for correctness and compatibility.
  • Add a “session indexer” that mirrors transcripts into Postgres:
#

This gets you:

  • one DB to query across 12+ agents
  • unified dashboards
  • simple backups (DB + a smaller set of state files)
  • without needing to redesign compaction / branching / parent forks / offline behavior.

Also worth calling out: OpenClaw already has session maintenance controls (rotation/pruning/disk budgets) to keep “hundreds of JSONL files” from becoming unbounded:

If you do file the feature request: what maintainers will need

If you open a GitHub issue, include these specifics (it’ll massively increase the chance it’s actionable):

  • Proposed extension point shape (e.g. TranscriptBackend interface: read tree, append entry, compaction entry, list sessionIds, atomicity expectations)
  • Migration story: JSONL → DB import; DB → JSONL export (for portability)
  • Concurrency model (single-writer gateway? multi-process?)
  • Failure modes (DB down: do we fall back to JSONL? hard fail?)
  • Performance targets (hot path: per-message append must be cheap)
#

Issue link (you’ll need to create it): https://github.com/openclaw/openclaw/issues

One quick sanity check for your current patch attempt

Since you’re running the Homebrew install: are you also using the embedded Pi runtime for those agents? If any of your agents aren’t using pi-embedded-runner, your attempt.ts edits will never execute (even if you rebuild correctly). Running under pnpm gateway:watch makes this much easier to verify because you can add a loud log and watch restarts.

If you want, paste:

  • how you start each agent (config snippet showing their runner/runtime selection, if any)
  • the exact A2UI error (if you still want to build a full release)
    …and I can suggest the lowest-friction path to prototype the DB mirror (hybrid) or to scope a true DB transcript backend proposal.
barren copper
#

Thanks @Krill for the detailed guidance!

Clarification: We already have the hybrid setup

We're already mirroring sessions to PostgreSQL in real-time:

  • session-sync.py running as daemon
  • 260 messages across 13 sessions in DB
  • Real-time sync from .jsonl → PostgreSQL

Our actual problem:

Even with DB mirroring, OpenClaw still loads from .jsonl files on every request, which means:

  1. Token usage: Still loading 100k-200k tokens from files (not using DB's filtered data)
  2. Startup latency: Reading large .jsonl files on every agent wake-up
  3. Can't leverage DB optimizations: We have indexed, filtered data in DB but can't use it

What we want:

Make OpenClaw load sessions from DB instead of .jsonl:

  • Query only recent 30 messages from DB (10k-20k tokens)
  • Use DB indexes for fast filtering
  • Leverage our existing PostgreSQL infrastructure

The question:

Is there a way to make SessionManager.open() use DB as the source, while keeping .jsonl as backup/export?

Or do we need to wait for official DB-backed SessionManager support?
We're happy to contribute the implementation if there's a clear extension point.

reef ploverBOT
#

You’re mixing two things that feel the same in practice but matter a lot architecturally:

  1. Where the transcript is persisted (JSONL vs DB)
  2. How much history gets sent to the model (this is what drives the 100k–200k token bills)

OpenClaw does not “spend tokens reading JSONL files” — tokens are spent when it rebuilds model context from the transcript and sends it to the LLM. Switching SessionManager.open() to DB only helps if it also changes the context selection policy (e.g. “only last 30 messages”).

Can SessionManager.open() use DB as the source today?

Not via a supported config/plugin knob, no.

Today SessionManager (from @mariozechner/pi-coding-agent) is fundamentally a “open a JSONL transcript file” thing, and OpenClaw’s embedded runner is wired around that. There isn’t an extension point like sessions.backend = postgres that you can drop in.

If you want true DB-backed sessions, it likely requires an upstream abstraction in Pi’s SessionManager (or a pretty invasive OpenClaw refactor). Upstream code lives here:
https://github.com/badlogic/pi-mono/tree/main/packages/coding-agent

The thing you want (“only recent 30 messages”) already exists — without DB

If your real pain is: “the model sees the whole transcript every turn”, the supported fix is compaction, tuned to keep only a small recent window:

#

Key idea: if you make reserveTokens large enough, Pi will compact much earlier (because it triggers when contextTokens > contextWindow - reserveTokens). After compaction, the model sees:

  • 1 compact summary
    • only the recent tail (size controlled by keepRecentTokens)

That gives you the “load only recent” effect for the model, which is what matters for token usage.

Two quick checks:

  • Does /status show any compactions for the expensive sessions?
  • Do your JSONLs contain type: "compaction" entries at all? If not, you’re paying full-history every time because nothing has compacted yet.

Also consider session pruning if tool output is what bloats you (it trims old tool results after cache TTL):
https://docs.openclaw.ai/concepts/session-pruning

If you still want DB-first persistence (not just smaller context)

#

Then yeah: you’re in “feature request + design work” territory.

What I’d suggest as the contribution path (highest chance of acceptance):

  1. Open a GitHub issue proposing a pluggable transcript backend (JSONL default; Postgres optional), explicitly noting this touches Pi’s SessionManager storage assumptions.
    https://github.com/openclaw/openclaw/issues
  2. In the issue, propose whether the abstraction belongs upstream in pi-coding-agent (preferred) or inside OpenClaw.
  3. Spell out required operations (append entry, load tree, compaction entries, repair/backup semantics, session header, etc.) and failure modes (DB down).

A pragmatic “DB-first without changing SessionManager” hack (if you want it)

Since you already mirror to DB: you could materialize a small “working set” JSONL (last N messages + optional summary) and point the session store’s sessionFile override at it (the store supports sessionFile). Then OpenClaw loads a tiny JSONL, while DB remains your full source of truth.

It’s hacky (IDs/tree semantics, replay correctness), but it’s the only way to get “DB-selected tail” into OpenClaw without changing SessionManager.

If you answer these two, I can be much more concrete about which lever you should pull next:

  1. Are you seeing any compaction events today (per /status)?
  2. Is your 100k–200k “tokens per wake-up” mostly tool output (browser/exec/read) or mostly pure chat history?
barren copper
#

Perfect! Here are the answers:

1) Compaction events:
âś… Yes! Current session has 2 compaction entries

  • OpenClaw recognizes them: logs show compactionSummary:1
  • tokensBefore: 180753
  • But still sends 107 messages (~100k+ tokens) to model

2) Token breakdown (current session):

  • Total messages: 496 (277 toolResult, 183 assistant, 36 user)
  • toolResult chars: 248,693 (~62k tokens)
  • user+assistant chars: 112,177 (~28k tokens)
  • Largest toolResults: 26KB, 25KB, 17KB (mostly read tool outputs)
    Answer: ~70% is tool output (read/exec), 30% is chat history

The core problem we're hitting:

Compaction can't run on live/active sessions (by design, to avoid corruption/interruption).

This means:

  • During active conversation, session keeps growing
  • Tokens spike from 30k → 100k+ quickly
  • Compaction only helps after session goes idle
  • By then, we've already burned tokens on the bloated context
#

Our goal:*

  • Always load minimal context (~30 recent messages, no toolResults)
  • On-demand history retrieval (agent queries DB when needed)
  • Works during active conversation (not just after compaction)

Why this needs DB-first:

  • Can't rely on periodic compaction (doesn't help live sessions)
  • Session pruning helps but doesn't give fine-grained control
  • Need: "dynamic context selection per turn" not "batch compaction after idle"

Question:
Is there a way to achieve "always minimal LLM context during active conversation" with current OpenClaw architecture?

Or does this require DB-backed SessionManager with dynamic context selection?
We're running 12+ agents with long-running sessions — this pattern hits us constantly.

reef ploverBOT
#

What you’re seeing (compaction summary exists, but the model still gets ~100k tokens) is consistent with “compaction happened, but the kept tail is still huge” — mostly because your tail is dominated by read/exec toolResults.

Two key clarifications:

1) Compaction can run on active sessions

It won’t run mid-turn (while a run is executing), but it absolutely can run between turns, and it can also run as “threshold maintenance” after a successful turn (that’s in the deep dive). So this isn’t “by design we can only compact when idle” — it’s “we only compact at certain thresholds / times”.

Deep dive (this is the authoritative behavior):
https://docs.openclaw.ai/reference/session-management-compaction

2) Why you still see 107 messages after compaction

Compaction doesn’t mean “keep last N messages”. It means:

  • persist 1 compaction summary entry
  • keep all messages after firstKeptEntryId
  • and the size of that kept region is governed by token-based knobs (keepRecentTokens, plus when compaction triggers via reserveTokens)
#

So if your recent window includes a few 17–26KB tool results (and lots of them), you can easily end up with 100k tokens even after compaction.


How to stop the “30k → 100k spike” during active work (with today’s features)

A) Make compaction trigger earlier (not only near the full context window)

Compaction triggers when contextTokens > contextWindow - reserveTokens.

So if you want to “cap” a session around ~20k-ish tokens, set reserveTokens high enough that the trigger point is ~20k below the model’s context window.

Example logic (numbers illustrative):

  • contextWindow ~200k
  • set reserveTokens ~180k
  • now the session compacts once it passes ~20k

This is the supported way to get “compact continuously while active” behavior.

#

B) Reduce what’s blowing up the tail: toolResults

Since ~70% of your bloat is read/exec tool output, compaction alone won’t feel good unless either:

  • the kept window is smaller or
  • toolResults inside the kept window are smaller/cleared

OpenClaw’s “session pruning” feature exists specifically to trim old tool results from the in-memory context:
https://docs.openclaw.ai/concepts/session-pruning

Even though the doc frames it as TTL/caching-oriented, it’s also the practical lever for “stop hauling giant tool outputs around forever”. If you want it to apply during active sessions, you can set the TTL very low (tradeoff: you give up some prompt caching benefit).

C) Reduce tool output at the source (biggest win)

Even with pruning, if agents keep dumping 20–50KB read outputs repeatedly, the “recent window” will always be expensive.

Pragmatic pattern that works well in multi-agent setups:

  • prefer small read slices (limit/offset) + targeted search (rg) over full-file reads
  • if you control any custom tools, return summaries + references rather than raw blobs
#

Next step (so this doesn’t stay theoretical)

Can you paste the relevant parts of your openclaw.json for:

  • agents.defaults.compaction (especially keepRecentTokens + any reserveTokens settings)
  • agents.defaults.contextPruning (if set)

And (from /status in that session) the current context token estimate + compaction count.

With those, I can tell you exactly which knob is letting 107 messages / 100k tokens slip through, and what to set to keep the live working set closer to your 10k–20k target.

#

Yes — what you’re describing (“dynamic context selection per turn”, always minimal, optionally DB-backed) is already a first‑class extension point in OpenClaw, and it’s not “DB-backed SessionManager”.

The built-in extension point you want: Context Engine plugins

OpenClaw can delegate context assembly (what messages get sent to the LLM each run), /compact, and related lifecycle to a pluggable context engine:

This is exactly how you implement:

  • “always keep last ~30 messages”
  • “drop toolResults (or replace them)”
  • “fetch older history from Postgres on demand”
  • “works during active conversation” (because it runs every turn at assemble-time)

What a context engine can do (actual interface)

Core types (straight from src/context-engine/types.ts in the repo):

#
  • assemble({ sessionId, messages, tokenBudget }) → { messages: AgentMessage[] }
  • ingest(...) / ingestBatch(...) to store turns into your DB
  • compact(...) if you want to own compaction semantics (or make it a no-op)

Source:
https://raw.githubusercontent.com/openclaw/openclaw/main/src/context-engine/types.ts

How to wire it up (high level)

  1. Write a plugin with a openclaw.plugin.json:
{
  "id": "db-context-engine",
  "kind": "context-engine",
  "configSchema": { "type": "object", "additionalProperties": true }
}
#
  1. In your plugin module, register the engine:
  1. Select it in config:
{
  plugins: { slots: { contextEngine: "db-context-engine" } }
}

Important gotcha: “no toolResults” must preserve provider validity

If you drop toolResult messages but leave assistant messages that contain tool calls, some providers will reject the prompt (tool-call/result pairing rules). So your engine should typically do one of:

  • Drop tool calls + tool results entirely from history (keep only pure user/assistant text turns), OR
  • Keep the tool call/result structure but hard-clear old tool result content (placeholder), OR
  • Replace tool results with a short summary message you generate/store in DB.
#

Do you need DB-backed SessionManager?

Not for your stated goal.

  • SessionManager = persistence format / transcript structure.
  • Context engine = per-turn “what do we send to the model?”

You can keep JSONL as canonical/backup/export, while your context engine:

  • ingests everything into Postgres,
  • assembles only the last 30 “clean” turns each run,
  • and optionally exposes a tool like history.fetch(sessionId, query) to pull older context on-demand.

If you want, I can sketch a minimal “last-30-no-tools” assemble() implementation strategy that won’t violate tool-call pairing rules — but first: do you want to drop all historical tool calls too, or keep tool calls and just blank tool results?

barren copper
#

@Krill Thanks for the detailed guidance! We've made progress but hit an issue:

What's Working:
âś… Context Engine plugin is loaded and registered correctly
âś… assemble() method is being called every turn (we see logs)
âś… Plugin can connect to PostgreSQL and query data

The Problem:
❌ OpenClaw still loads from .jsonl files, not from our DB data
❌ Even though assemble() returns DB messages, the LLM still receives the full .jsonl content

Evidence:
[DBContextEngine] assemble called: { sessionId: '42ae96f4-...', messageCount: 110 }
[DBContextEngine] loaded from DB: 20 messages
// But gateway log shows:
[context-diag] messages=110 historyTextChars=97312 sessionFile=...jsonl

Our Understanding:
It seems assemble() is called AFTER OpenClaw loads the .jsonl file, and our returned messages don't actually replace what gets sent to the LLM?

Questions:
Does assemble() fully replace the messages sent to the LLM, or just filter them?
Are we missing a config to make OpenClaw use the Context Engine's output?
Should we implement ingest() to write to DB instead of relying on external sync?

Our Setup:
OpenClaw 2026.3.9
Plugin: ~/.openclaw/plugins/db-context-engine/
Config: plugins.slots.contextEngine: "db-context-engine"
Plugin manifest: kind: "context-engine"
Any guidance would be appreciated! 🙏

reef ploverBOT
#

You’re not missing a config — assemble() is supposed to fully replace what gets sent to the LLM.

And the [context-diag] line you’re looking at is definitive: it logs activeSession.messages right before activeSession.prompt(...), so if it says messages=110, then your assembled 20 messages did not make it into activeSession.agent.

Why this is happening (very likely)

In 2026.3.9 the integration is:

const assembled = await params.contextEngine.assemble({ ... });
if (assembled.messages !== activeSession.messages) {
  activeSession.agent.replaceMessages(assembled.messages);
}

(Source: src/agents/pi-embedded-runner/run/attempt.ts, around the params.contextEngine.assemble call.)

That !== is a reference check, not a deep compare.

#

So if your engine does either of these:

  • returns the same array instance it was passed (return { messages, ... }), even if you mutated it, or
  • mutates the passed messages array “in place” and returns it

…then OpenClaw will skip replaceMessages(), and the prompt will still use the original history.

This matches your symptoms perfectly: assemble runs, you log “loaded 20 from DB”, but prompt still shows 110.

Quick verification (add this log)

Inside your assemble():

api.logger.info(
  `[DBContextEngine] assemble: sameRef=${resultMessages === params.messages} ` +
  `inLen=${params.messages.length} outLen=${resultMessages.length}`
#
);

If sameRef=true, that’s the bug.

The fix

Make sure you never return the input array, and never rely on mutating it. Always return a fresh array object:

const dbMsgs = await loadFromDb(...);        // however you do it
const out = dbMsgs.map(toAgentMessage);      // must be real AgentMessage objects
return {
  messages: out,                             // NEW array reference
  estimatedTokens: estimate(out),
};
#

For an even simpler “does replacement work?” test, ignore DB entirely and just do:

return { messages: params.messages.slice(-5), estimatedTokens: 0 };

If your next [context-diag] becomes messages=5, your engine wiring is correct and the remaining work is just “return a new array + correct AgentMessage shape”.

Two other things to watch for

  1. Are you seeing this warning?
    context engine assemble failed, using pipeline messages: ...
    If your DB messages aren’t valid AgentMessage objects (wrong shape), your engine might throw after logging “loaded 20”, and OpenClaw will fall back to the pipeline messages (110).

  2. SessionManager still opens JSONL
    Even with a context engine, OpenClaw will still open the session file for persistence (that’s separate from context selection). The context engine controls what goes to the LLM, not whether JSONL is touched at all.

Re: ingest()

#

You don’t need ingest() for assemble replacement to work, but if you want to stop relying on an external sync daemon, implementing ingest() / ingestBatch() is the clean “own the pipeline” approach.

If you paste your current assemble() implementation (just that function body), I can point out exactly where you’re accidentally returning the same array ref or producing non-AgentMessage objects.

barren copper
#

My agent failed to reply me with the error message: 02:37:15 [agent/embedded] embedded run prompt end: runId=c8fb008f-4ca9-4a95-aa42-3ff58e332ce9 sessionId=1d2789d7-4d06-477c-adf5-6f350f78cf49 durationMs=9
02:37:15 [diagnostic] session state: sessionId=1d2789d7-4d06-477c-adf5-6f350f78cf49 sessionKey=agent:main:discord:channel:1475796349118054561 prev=processing new=idle reason="run_completed" queueDepth=0
02:37:15 [diagnostic] run cleared: sessionId=1d2789d7-4d06-477c-adf5-6f350f78cf49 totalActive=0
02:37:15 [diagnostic] lane task error: lane=main durationMs=145 error="TypeError: Cannot read properties of undefined (reading 'totalTokens')"
02:37:15 [diagnostic] lane task error: lane=session:agent:main:discord:channel:1475796349118054561 durationMs=163 error="TypeError: Cannot read properties of undefined (reading 'totalTokens')"
02:37:15 Embedded agent failed before reply: Cannot read properties of undefined (reading 'totalTokens')

reef ploverBOT
#

That TypeError: Cannot read properties of undefined (reading 'totalTokens') is the smoking gun: you’ve now successfully changed the history enough that some assistant message in the assembled context is missing a usage object, and something in the embedded runner / pi runtime expects:

assistantMessage.usage.totalTokens

When usage is undefined, you get exactly this crash — and because it happens before reply delivery completes, you see “Embedded agent failed before reply…”.

Why this suddenly shows up with a DB context engine

When you “re-hydrate” messages from Postgres you’re almost certainly not reconstructing the full AgentMessage shape that Pi/OpenClaw expects (especially for assistant messages). In particular:

  • assistant messages typically carry a usage snapshot (at least { totalTokens: number, ... })
  • tool call/result pairing + various sanitization expectations also exist

If you return “simplified” messages from DB, they may look fine for role/content, but they’re structurally incomplete for the runner’s accounting code.

Fastest fix (recommended): don’t reconstruct messages yet

#

For your first working version, ignore DB for the actual message objects and just filter what OpenClaw already loaded:

assemble: async (params) => {
  const out = params.messages.slice(-30); // or filter out toolResults, etc.
  return { messages: out, estimatedTokens: 0 };
}

If that runs without crashing and [context-diag] drops, your context engine wiring is correct.

Then, use DB only to decide which IDs to keep, but return the original message objects from params.messages (so you keep usage, provider-specific fields, etc.).

If you insist on constructing DB messages right now

Then you must ensure every assistant message you return has a usage object that includes totalTokens (and ideally the full shape). Minimal safe placeholder pattern:

#
usage: {
  input: 0, output: 0, cacheRead: 0, cacheWrite: 0,
  totalTokens: 0,
  cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }
}

(Yes, it’s annoying — but it prevents the crash.)

Two quick things to check in your logs

  1. Do you see the warning:
    context engine assemble failed, using pipeline messages: ...
    If yes, your assemble is throwing and you’re falling back.

  2. Right before returning from assemble(), log:

  • how many assistant messages you’re returning
#
  • how many have usage missing

If you paste just your assemble() implementation (or a sanitized excerpt of the message object you’re returning for an assistant turn), I can point out exactly what field is missing / wrong so you don’t have to guess.

barren copper
#

Thank problem solved.

Next

Context engine "db-context-engine" is not registered. Available engines: legacy
reef ploverBOT
#

That error is coming from resolveContextEngine() when it tries to look up the engine id from plugins.slots.contextEngine and only finds the built-in legacy engine:

Context engine "db-context-engine" is not registered. Available engines: legacy

Meaning: your plugin module did not successfully run api.registerContextEngine("db-context-engine", …) during plugin load (or it registered under a different id).

Checklist to fix (in order)

1) Confirm the plugin is actually loading, not just “discoverable”

Run:

  • openclaw plugins list

You should see db-context-engine as enabled and a status like “loaded/ok” (wording varies). If it’s missing or “failed”, check the gateway logs right around startup for a [plugins] ... failed to load ... line.

Important nuance: config validation can succeed if the manifest exists, even if the plugin entry module fails to import — and context engines only get registered when the entry module runs.

#

2) Make sure the plugin entry file is exactly one of these

OpenClaw’s default plugin entry candidates are:

  • index.ts
  • index.js
  • index.mjs
  • index.cjs

…and it must live in the plugin root next to openclaw.plugin.json.

So your plugin directory should look like:

~/.openclaw/plugins/db-context-engine/
  openclaw.plugin.json
  index.ts
#

If your code is in src/index.ts or similar, it won’t be loaded unless you explicitly configure extension entries (different mechanism).

3) Make sure you register the engine id that matches the slot value

Your config says:

plugins: { slots: { contextEngine: "db-context-engine" } }

So your plugin must do:

api.registerContextEngine("db-context-engine", () => /* engine */);

If you registered "dbContextEngine" / "db-context" / etc, you’ll get exactly this error.

#

A safe pattern is:

api.registerContextEngine(api.id, () => /* ... */);

(as long as your plugin id really is db-context-engine).

4) Minimal working plugin skeleton (sanity test)

In index.ts:

import type { OpenClawPluginDefinition } from "openclaw/plugin-sdk/core";

const plugin: OpenClawPluginDefinition = {
  id: "db-context-engine",
#
  kind: "context-engine",
  register(api) {
    api.registerContextEngine("db-context-engine", async () => ({
      info: { id: "db-context-engine", name: "DB Context Engine" },
      ingest: async () => ({ ingested: false }),
      assemble: async ({ messages }) => ({ messages, estimatedTokens: 0 }),
      compact: async () => ({ ok: true, compacted: false }),
    }));
  },
};

export default plugin;

Then restart the gateway.

#

If you paste the contents of your openclaw.plugin.json + the first ~40 lines of your index.ts (especially the export default … shape), I can spot immediately whether it’s (a) wrong entry filename/location, (b) plugin not enabled/loaded, or (c) id mismatch between slot and registerContextEngine().

barren copper
#

Thanks again for all the guidance — we’re still seeing two blockers and would like to ask for maintainer-level advice.

Current status

  • db-context-engine plugin is now being loaded and assemble() is called.
  • Session sync to Postgres is working (manual backfill validated, session_messages.count matches expected).
  • However, we still intermittently hit:
    • No tool call found for function call output with call_id ...
    • messages returned 0 rows after retry/replay for some session keys.

What we observed

  1. Session key / replay consistency

    • Some sessions can be recovered from DB and replayed correctly.
    • After /new or reset, context may switch to a new session id and not auto-load prior messages.
    • We added fallback logic, but behavior is still inconsistent in real runs.
  2. Runtime mismatch suspicion

    • Disk plugin code appears updated.
    • Logs sometimes still show old behavior/old log wording, which suggests gateway process might not always be using the expected plugin instance/version.
  3. Protocol-level failure (No tool call found...)

    • This seems to be state desync between tool-call registration and function-call output handling.
    • In affected runs, only session reset/new reliably recovers.

Questions for maintainers

  1. Is No tool call found for function call output... a known race condition around session reset/stream interruption/concurrency boundaries?
  2. What is the recommended way to verify exactly which plugin path/version is loaded at runtime (single source of truth in logs/diagnostics)?
  3. For cross-session continuity, what is the officially recommended approach:
    • lock channel to stable session id, or
    • replay last N messages from previous session on new-session startup?
  4. Is there an official hook point for “startup replay from previous session” that won’t fight the core session router?
#

Environment

  • OpenClaw: 2026.3.9
  • Gateway: LaunchAgent daemon on macOS
  • Runtime path: /opt/homebrew/lib/node_modules/openclaw/dist/index.js
  • Postgres-backed session mirror enabled

If helpful, we can provide a minimal reproducible sequence with timestamps and sanitized logs.

reef ploverBOT
#

Those two errors are both consistent with “custom context assembly/replay is occasionally producing an invalid tool-call transcript or racing the session-id lifecycle”, rather than a core “DB-first” problem.

I’ll go question-by-question and tie each one to the actual hook points OpenClaw gives you.


1) No tool call found for function call output with call_id ...

This is almost always tool-call/result pairing drift.

In OpenAI-style tool calling (and several strict providers), the history must satisfy:

  • an assistant message contains a tool/function call with a specific call_id
  • later, a tool output/result references that same call_id
  • if you include the output without the matching call (or vice versa), the provider/runtime rejects it with exactly this kind of error

In OpenClaw’s embedded runner, the built-in pipeline does a repair pass after truncation (sanitizeToolUseResultPairing(...)) before your context engine runs. If your context engine then:

  • drops tool-call messages but keeps tool-result messages, or
#
  • replays tool results from DB without also replaying the corresponding assistant tool call,
    you can reintroduce the mismatch.

Recommendation: in your context engine assemble() either:

  • drop tool calls and tool results together (and also drop assistant messages that contain tool-call blocks), or
  • keep full pairs, but never allow orphan tool results to survive.

If you want a quick “stop the bleeding” option: start by assembling only user/assistant text turns from the already-sanitized params.messages array (no DB rehydrate), then reintroduce DB replay once it’s stable.


2) Verifying which plugin path/version is loaded (single source of truth)

There are three practical ways:

A) openclaw plugins list

This should show the plugin id and where it was loaded from. If you have two copies with the same id in different discovery locations, precedence rules apply and you’ll get “why is it running old code?” symptoms.

#

B) Log api.source + api.version at plugin register time

In your plugin’s register() do something like:

api.logger.info(`[db-context-engine] loaded source=${api.source} version=${api.version}`);

That line in the gateway log is the best “ground truth”.

C) Disable plugin discovery/manifest caches while iterating

OpenClaw has short startup caches for plugin discovery/manifest reads. For debugging iteration, set:

  • OPENCLAW_DISABLE_PLUGIN_DISCOVERY_CACHE=1
  • OPENCLAW_DISABLE_PLUGIN_MANIFEST_CACHE=1

(Those env vars are documented in the plugins guide.)

Also: make sure you don’t have two gateways running (LaunchAgent + a foreground dev gateway). That’s the other common reason logs “flip” between old/new wording.


#

3) Cross-session continuity: stable session id vs replay

OpenClaw’s “conversation bucket” is the sessionKey. The sessionId is expected to change on:

  • /new and /reset
  • daily reset boundary
  • idle expiry (if configured)

So “lock channel to stable session id” fights the design.

Officially-aligned approach: treat a new sessionId as a new transcript, and carry continuity via one of:

  • memory (durable notes in workspace; best for long-running multi-agent ops), or
  • your context engine injecting a summary/seed context into the new session.

If your goal is “new session starts, but preload some prior context”, don’t try to resurrect the old sessionId — instead do “carryover”.


4) Official hook point for “startup replay from previous session”

#

You have two viable places that don’t fight the router:

A) Context engine bootstrap()

This is meant for “initialize/import on session start”, and OpenClaw calls it early in the embedded runner.

Caveat: today it’s only called when hadSessionFile is true. On a brand new session file (common right after /new), it won’t fire, so you may need to handle “DB empty” inside assemble().

B) Plugin hooks before_reset / session_start

Plugin hooks exist for these lifecycle moments (they’re in the plugin hook names list). Pattern:

  • on before_reset: write a “carryover summary pointer” keyed by sessionKey
  • on session_start: record (sessionKey, sessionId) mapping (so your context engine can query DB by sessionId and still find the sessionKey lineage)

One more observation about your “0 rows after retry/replay”

If you’re relying on an external sync daemon, it’s very easy for the embedded runner to call assemble() for a new sessionId before your daemon has inserted anything → “0 rows”.

#

If you want DB replay to be deterministic, the clean fix is: implement ingest() / ingestBatch() in the context engine and stop relying on external sync latency for “current session” correctness.

Also note: in attempt.ts, if you implement afterTurn(), OpenClaw will not do the fallback ingest for you — so you must persist inside afterTurn() yourself.


If you can share:

  • which provider/model API path you’re using when the call_id error happens (OpenAI Responses vs Anthropic vs Google), and
  • a short sample of the assembled message sequence around the failure (just roles + whether it contains tool_call/tool_result + call_id),
    I can tell you exactly which pairing invariant is being broken and what filter rule to implement in assemble() to make it impossible.
#

Yes — a minimal repro + sanitized logs would be the fastest way to get this to “maintainer-actionable”.

Given you’re now at the point where it’s either (a) a subtle pairing invariant violation in your assemble() output, or (b) a real core bug, I’d suggest doing both:

  1. paste the repro + logs here (so we can sanity-check the invariants quickly), and
  2. open a GitHub issue once it’s distilled (that’s the path for maintainer-level fixes): https://github.com/openclaw/openclaw/issues

What to include in the minimal repro (high signal, low noise)

Please include one failing sessionKey and a short timeline:

  • OpenClaw version + commit (openclaw --version already great)
  • Output of: openclaw plugins list (so we see which copy of the plugin is actually loaded + its source path)
  • The exact plugins.slots.contextEngine value from your config
  • A log excerpt covering:
    • the [context-diag] pre-prompt ... line
    • your plugin’s assemble() log showing input len / output len (and ideally whether you’re including any tool calls/results)
    • the first occurrence of No tool call found for function call output with call_id ...
#
  • (if present) any /new / reset boundary lines showing old/new sessionId

One extra thing that makes the tool-call error immediately diagnosable

In your context engine, dump a sanitized summary of the assembled history like:

  • count of assistant messages containing tool calls (and the call_ids)
  • count of tool result messages (and the call_ids)

If the tool-result call_id set isn’t a subset of the tool-call call_id set, the failure is guaranteed — and we can point to the exact filter rule to fix it.

Re: session-id continuity

It’s expected that /new mints a new sessionId. For continuity, you’ll want to key your DB lineage on sessionKey, and maintain a mapping (sessionId → sessionKey) so your engine can “carry over” (summary or last-N) on the first run of a fresh sessionId.


If you paste the above (even redacted), I can tell you which side it’s on:

#
  • plugin output invariant violation (most likely) → fix in assemble() pairing rules, or
  • core bug → issue-worthy with your repro attached.
barren copper
#

Update: distilled evidence (maintainer-actionable)

Environment:

  • OpenClaw: 2026.3.9 (20d097a)
  • Context engine slot: plugins.slots.contextEngine = db-context-engine
  • Plugin status confirms custom engine is loaded from:
    ~/.openclaw/plugins/db-context-engine/index.js
  • openclaw plugins list shows DB Context Engine status = loaded (not just discoverable)

Observed failure mode:

  1. Repeated warning during assembly:
    [DBContextEngine] agent fallback unavailable; reason=column "agent_id" does not exist

  2. Correlated runs show stable session keys and pre-prompt activity, e.g.:

  • sessionKey: agent:main:cron:b42ba400-744d-4a21-805b-e8d7d6993ce8
  • sessionId: 67515428-b5d5-4d67-9e81-1e47896c63f2
  • window: 2026-03-12 18:44:16 ~ 18:44:22 Europe/London
  • repeated [context-diag] pre-prompt ... + repeated fallback warning above
  1. Additional active lane also shows same schema mismatch warning:
  • sessionKey: agent:main:discord:channel:1475796349118054561
  • sessionId: 29987163-75ad-4636-b4f8-de59a71ed110
  • window: 18:54:51 ~ 18:56:40
  1. During later collection, CLI intermittently reports:
    Gateway not reachable. Is it running and accessible?
    (target ws://127.0.0.1:18789)

Interpretation:

  • Custom context engine is definitely loaded.
  • Current fallback path assumes session_messages.agent_id, but this column does not exist in this deployment schema.
  • There may also be a gateway lifecycle/connectivity issue that affects reproducibility/log capture.

Questions:

  1. Should context-engine fallback rely on agent_id in session_messages, or is another canonical linkage expected?
  2. Is there a schema contract/versioned migration for session_messages that plugins should target?
  3. Best-practice hook for startup continuity after /new (session reset): summary/last-N replay from prior lineage?
  4. Any known relation between transient gateway reachability and tool-call/output pairing errors?
reef ploverBOT
#

This update is very actionable — and it points to two separate issues:

  1. your plugin’s own DB schema assumptions (agent_id), and
  2. gateway process stability/reachability (which can absolutely make tool-call pairing failures look “random”).

I’ll answer your four questions directly.


1) Should a context-engine fallback rely on session_messages.agent_id?

No — OpenClaw core does not define (or rely on) a session_messages table at all.

From the core context-engine contract, the only guaranteed identifier you get consistently is sessionId (plus sometimes sessionFile via bootstrap/afterTurn/compact). Core doesn’t guarantee that “agent id” is present in your DB, because the DB is plugin-owned.

So if your engine’s “fallback” wants agent-scoped retrieval, the canonical way is:

  • store rows keyed by sessionId, and
#
  • separately maintain a mapping sessionId -> sessionKey (or agentId) inside your plugin/DB, populated via hooks/events (see below).

That’s why your current warning is happening: your plugin is trying to do an “agent-level fallback query” but your deployment schema doesn’t have that column.


2) Is there a schema contract / versioned migration for session_messages plugins should target?

As of 2026.3.9: no official core contract for a session_messages SQL schema.

If you want something “portable” across deployments, the plugin needs to own it:

  • define a schema version table (e.g. db_context_engine_meta(version, applied_at)),
  • ship migrations (even if it’s “run these SQL files manually” at first),
  • and treat all cross-deployment assumptions (like agent_id) as optional unless you migrate for them.

If you want maintainer buy-in for a standardized schema, that’s a GitHub issue proposal (core + plugin SDK implications).


#

3) Best-practice for continuity after /new (reset)

Core behavior is: sessionKey stays, sessionId changes. Don’t fight that.

The clean “officially-aligned” pattern is:

  • On before_reset (plugin hook) capture a carryover summary for the old sessionId keyed by sessionKey.
  • On session_start (plugin hook) record the new (sessionKey, sessionId) mapping.
  • In your context engine assemble(), if the session is “fresh” (few messages), inject that carryover as systemPromptAddition (preferred) or as a synthetic “summary” message.

This avoids replaying raw tool-call transcripts (which is where call_id pairing problems usually come from).


4) Any relation between transient gateway reachability and tool-call pairing errors?

Yes, very plausibly.

If the gateway is intermittently unreachable at ws://127.0.0.1:18789, you may have:

#
  • gateway restarts/crashes,
  • two competing gateway processes (LaunchAgent + manual run),
  • or a deadlock/hang (DB connection attempt, plugin init, etc.)

Any mid-run restart/interruption can leave you with partially persisted turns and/or tool-call/result mismatches that then surface as “No tool call found …”.

Actionable check: in the gateway log around those windows, look for restart signatures (startup banners, plugin load lines repeating, port bind failures). Also confirm there’s only one gateway owning 18789.


Immediate unblock for your current warning

Either:

  • add agent_id (and populate it) in your DB schema, or
  • change the fallback to not require agent_id and use your own mapping table keyed by sessionId/sessionKey (recommended), or
  • drop “agent fallback” entirely and just use sessionId-scoped history until the mapping is implemented.

Given you’ve already got plugin hooks available, I’d do the mapping-table approach.

#

If you want, paste the SQL schema for session_messages (columns only, no data) + the exact fallback query you’re running, and I’ll suggest a minimal schema/mapping that works with OpenClaw’s sessionKey/sessionId lifecycle without needing core changes.

barren copper
#

Thanks — this is super clear and aligns with what we’re observing.

We’ll treat these as two separate tracks:

  1. plugin-owned DB contract issue (agent_id assumption), and
  2. gateway reachability/stability issue affecting reproducibility.

Immediate plan:

  • remove hard dependency on session_messages.agent_id,
  • move to sessionId-scoped retrieval + mapping table (sessionId -> sessionKey),
  • use carryover summary injection on fresh /new sessions instead of raw tool transcript replay,
  • verify single gateway ownership of port 18789 and check restart signatures around failing windows.

We’ll post a follow-up with schema + fallback query and the resulting patch.

reef ploverBOT
#

Sounds like the right split.

When you come back with schema + query, two small “do it once, don’t regret it later” suggestions for the mapping/carryover track:

1) Mapping table: keep it brutally simple + indexed

A minimal shape that tends to work well:

  • session_id (PK; UUID/text)
  • session_key (text, indexed)
  • agent_id (text, optional but useful; can be derived from session_key like agent:<id>:...)
  • created_at (timestamp)
  • optional: prev_session_id (UUID/text) if you want explicit lineage across /new

Index: (session_key, created_at desc) so “latest session for this key” is cheap.

2) Prefer plugin hooks for “sessionKey exists” moments

Your context engine only gets sessionId in assemble(), so the cleanest way to populate sessionId -> sessionKey is via plugin hooks that do have routing context (e.g. session_start, before_reset). Then your engine can query the mapping by sessionId.

#

3) Carryover injection: use systemPromptAddition over replaying old tool transcripts

That avoids tool-call pairing invariants entirely and keeps the provider turn structure clean.

4) Gateway stability: verify “one owner of 18789” before debugging anything else

On macOS it’s easy to accidentally have LaunchAgent + a foreground run fighting. Checking restart signatures around the failing windows will likely explain a lot of “intermittent” behavior.

Drop the schema + fallback query when ready and we can sanity-check the join keys and any remaining failure modes.

barren copper
#

Thanks @Krill — quick final update: we’ve resolved this on our side ✅

Root cause
Our context-engine plugin config wasn’t being injected correctly at runtime.
The engine fell back to default values (keepRecentMessages=30) and clipped context unexpectedly.
What we fixed
Corrected plugin config injection path.
Verified runtime logs now show expected values (e.g. keepRecentMessages=100).
Confirmed DB-first context assembly is active and no longer stuck at 30 rows.
Current status
DB-first flow is working for our current setup.
We’ll continue refining and then migrate the same pattern to our Docker agents.
Thanks again for the guidance — marking this as solved now.

reef ploverBOT
#

Nice — that explains the “it keeps behaving like 30 rows no matter what” weirdness.

If you’re up for it, it’d be super helpful for future folks if you drop a 3–5 line snippet of the exact config location you corrected (just the relevant plugins.entries... / plugins.slots... part, sanitized). That’s exactly the kind of thing that bites people.

And if this thread is solved on your end: please right-click the solution message → Apps → ✅ Mark Solution so Answer Overflow indexes it for others.

barren copper
#

Yep — here’s the sanitized bit that mattered:

plugins:
entries:

  • path: ~/.openclaw/plugins/db-context-engine
    config: { keepRecentMessages: 100 }

slots:
contextEngine: db-context-engine
The key fix was: put the row limit under that plugin entry’s config, and ensure the slot is actually bound to db-context-engine (otherwise it silently falls back to defaults like 30).

reef ploverBOT
#

Thanks for posting the sanitized snippet — that’s the exact “gotcha” others will hit.

For anyone finding this thread later, the key points are:

  • openclaw.json is JSON5 (not YAML), and plugin-specific settings live under:
    • plugins.entries.<pluginId>.config
  • The active context engine is selected via:
    • plugins.slots.contextEngine = "<pluginId>"

So the canonical shape is basically:

{
  plugins: {
    slots: { contextEngine: "db-context-engine" },
    entries: {
#
      "db-context-engine": {
        // enabled: true,   // depending on how you load/install it
        config: { keepRecentMessages: 100 },
      },
    },
  },
}

If you haven’t already: please right-click the solution message → Apps → ✅ Mark Solution so Answer Overflow indexes it.

barren copper
#

Great callout, Krill — and thanks for the clean canonical JSON5 shape.

You’re absolutely right on both points:

openclaw.json is JSON5 (not YAML)
The effective wiring is:
plugins.slots.contextEngine = "<pluginId>"
plugins.entries.<pluginId>.config = { ... }
In our case, the confusion came from mixing loading styles (path/list-style vs keyed entries object), which made it look like config updates were ignored and everything stayed at default behavior.

Appreciate you documenting this for others — this is exactly the kind of subtle config trap that costs hours.