* fix(agents): add custom api registry helper
* fix(ollama): register native api for embedded runs
* fix(ollama): register custom api before compaction
* fix(tts): register custom api before summarization
* changelog: note ollama compaction registration fix
* fix(ollama): honor resolved base urls in custom api paths
* feat(context-engine): add ContextEngine interface and registry
Introduce the pluggable ContextEngine abstraction that allows external
plugins to register custom context management strategies.
- ContextEngine interface with lifecycle methods: bootstrap, ingest,
ingestBatch, afterTurn, assemble, compact, prepareSubagentSpawn,
onSubagentEnded, dispose
- Module-level singleton registry with registerContextEngine() and
resolveContextEngine() (config-driven slot selection)
- LegacyContextEngine: pass-through implementation wrapping existing
compaction behavior for 100% backward compatibility
- ensureContextEnginesInitialized() guard for safe one-time registration
- 19 tests covering contract, registry, resolution, and legacy parity
* feat(plugins): add context-engine slot and registerContextEngine API
Wire the ContextEngine abstraction into the plugin system so external
plugins can register context engines via the standard plugin API.
- Add 'context-engine' to PluginKind union type
- Add 'contextEngine' slot to PluginSlotsConfig (default: 'legacy')
- Wire registerContextEngine() through OpenClawPluginApi
- Export ContextEngine types from plugin-sdk for external consumers
- Restore proper slot-based resolution in registry
* feat(context-engine): wire ContextEngine into agent run lifecycle
Integrate the ContextEngine abstraction into the core agent run path:
- Resolve context engine once per run (reused across retries)
- Bootstrap: hydrate canonical store from session file on first run
- Assemble: route context assembly through pluggable engine
- Auto-compaction guard: disable built-in auto-compaction when
the engine declares ownsCompaction (prevents double-compaction)
- AfterTurn: post-turn lifecycle hook for ingest + background
compaction decisions
- Overflow compaction: route through contextEngine.compact()
- Dispose: clean up engine resources in finally block
- Notify context engine on subagent lifecycle events
Legacy engine: all lifecycle methods are pass-through/no-op, preserving
100% backward compatibility for users without a context engine plugin.
* feat(plugins): add scoped subagent methods and gateway request scope
Expose runtime.subagent.{run, waitForRun, getSession, deleteSession}
so external plugins can spawn sub-agent sessions without raw gateway
dispatch access.
Uses AsyncLocalStorage request-scope bridge to dispatch internally via
handleGatewayRequest with a synthetic operator client. Methods are only
available during gateway request handling.
- Symbol.for-backed global singleton for cross-module-reload safety
- Fallback gateway context for non-WS dispatch paths (Telegram/WhatsApp)
- Set gateway request scope for all handlers, not just plugin handlers
- 3 staleness tests for fallback context hardening
* feat(context-engine): route /compact and sessions.get through context engine
Wire the /compact command and sessions.get handler through the pluggable
ContextEngine interface.
- Thread tokenBudget and force parameters to context engine compact
- Route /compact through contextEngine.compact() when registered
- Wire sessions.get as runtime alias for plugin subagent dispatch
- Add .pebbles/ to .gitignore
* style: format with oxfmt 0.33.0
Fix duplicate import (ControlUiRootState in server.impl.ts) and
import ordering across all changed files.
* fix: update extension test mocks for context-engine types
Add missing subagent property to bluebubbles PluginRuntime mock.
Add missing registerContextEngine to lobster OpenClawPluginApi mock.
* fix(subagents): keep deferred delete cleanup retryable
* style: format run attempt for CI
* fix(rebase): remove duplicate embedded-run imports
* test: add missing gateway context mock export
* fix: pass resolved auth profile into afterTurn compaction
Ensure the embedded runner forwards resolved auth profile context into
legacy context-engine compaction params on the normal afterTurn path,
matching overflow compaction behavior. This allows downstream LCM
summarization to use the intended provider auth/profile consistently.
Also fix strict TS typing in external-link token dedupe and align an
attempt unit test reasoningLevel value with the current ReasoningLevel
enum.
Regeneration-Prompt: |
We were debugging context-engine compaction where downstream summary
calls were missing the right auth/profile context in normal afterTurn
flow, while overflow compaction already propagated it. Preserve current
behavior and keep changes additive: thread the resolved authProfileId
through run -> attempt -> legacy compaction param builder without
broad refactors.
Add tests that prove the auth profile is included in afterTurn legacy
params and that overflow compaction still passes it through run
attempts. Keep existing APIs stable, and only adjust small type issues
needed for strict compilation.
* fix: remove duplicate imports from rebase
* feat: add context-engine system prompt additions
* fix(rebase): dedupe attempt import declarations
* test: fix fetch mock typing in ollama autodiscovery
* fix(test): add registerContextEngine to diffs extension mock APIs
* test(windows): use path.delimiter in ios-team-id fixture PATH
* test(cron): add model formatting and precedence edge case tests
Covers:
- Provider/model string splitting (whitespace, nested paths, empty segments)
- Provider normalization (casing, aliases like bedrock→amazon-bedrock)
- Anthropic model alias normalization (opus-4.5→claude-opus-4-5)
- Precedence: job payload > session override > config default
- Sequential runs with different providers (CI flake regression pattern)
- forceNew session preserving stored model overrides
- Whitespace/empty model string edge cases
- Config model as string vs object format
* test(cron): fix model formatting test config types
* test(phone-control): add registerContextEngine to mock API
* fix: re-export ChannelKind from config-reload-plan
* fix: add subagent mock to plugin-runtime-mock test util
* docs: add changelog fragment for context engine PR #22201
createOllamaStreamFn() only accepted baseUrl, ignoring custom headers
configured in models.providers.<provider>.headers. This caused 403
errors when Ollama endpoints are behind reverse proxies that require
auth headers (e.g. X-OLLAMA-KEY via HAProxy).
Add optional defaultHeaders parameter to createOllamaStreamFn() and
merge them into every fetch request. Provider headers from config are
now passed through at the call site in the embedded runner.
Fixes#24285
* feat(hooks): add trigger and channelId to plugin hook agent context
Adds `trigger` and `channelId` fields to `PluginHookAgentContext` so
plugins can determine what initiated the agent run and which channel
it originated from, without session-key parsing or Redis bridging.
trigger values: "user", "heartbeat", "cron", "memory"
channelId values: "telegram", "discord", "whatsapp", etc.
Both fields are threaded through run.ts and attempt.ts hookCtx so all
hook phases receive them (before_model_resolve, before_prompt_build,
before_agent_start, llm_input, llm_output, agent_end).
channelId falls back from messageChannel to messageProvider when the
former is not set. followup-runner passes originatingChannel so queued
followup runs also carry channel context.
* docs(changelog): note hook context parity fix for #28623
---------
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* fix(plugins): expose ephemeral sessionId in tool contexts for per-conversation isolation
The plugin tool context (`OpenClawPluginToolContext`) and tool hook
context (`PluginHookToolContext`) only provided `sessionKey`, which
is a durable channel identifier that survives /new and /reset.
Plugins like mem0 that need per-conversation isolation (e.g. mapping
Mem0 `run_id`) had no way to distinguish between conversations,
causing session-scoped memories to persist unbounded across resets.
Add `sessionId` (ephemeral UUID regenerated on /new and /reset) to:
- `OpenClawPluginToolContext` (factory context for plugin tools)
- `PluginHookToolContext` (before_tool_call / after_tool_call hooks)
- Internal `HookContext` for tool call wrappers
Thread the value from the run attempt through createOpenClawCodingTools
→ createOpenClawTools → resolvePluginTools and through the tool hook
wrapper.
Closes#31253
Made-with: Cursor
* fix(agents): propagate embedded sessionId through tool hook context
* test(hooks): cover sessionId in embedded tool hook contexts
* docs(changelog): add sessionId hook context follow-up note
* test(hooks): avoid toolCallId collision in after_tool_call e2e
---------
Co-authored-by: SidQin-cyber <sidqin0410@gmail.com>
* fix(hooks): deduplicate after_tool_call hook in embedded runs
(cherry picked from commit c129a1a74b)
* fix(hooks): propagate sessionKey in after_tool_call context
The after_tool_call hook in handleToolExecutionEnd was passing
`sessionKey: undefined` in the ToolContext, even though the value is
available on ctx.params. This broke plugins that need session context
in after_tool_call handlers (e.g., for per-session audit trails or
security logging).
- Add `sessionKey` to the `ToolHandlerParams` Pick type
- Pass `ctx.params.sessionKey` through to the hook context
- Add test assertion to prevent regression
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
(cherry picked from commit b7117384fc)
* fix(hooks): thread agentId through to after_tool_call hook context
Follow-up to #30511 — the after_tool_call hook context was passing
`agentId: undefined` because SubscribeEmbeddedPiSessionParams did not
carry the agent identity. This threads sessionAgentId (resolved in
attempt.ts) through the session params into the tool handler context,
giving plugins accurate agent-scoped context for both before_tool_call
and after_tool_call hooks.
Changes:
- Add `agentId?: string` to SubscribeEmbeddedPiSessionParams
- Add "agentId" to ToolHandlerParams Pick type
- Pass `agentId: sessionAgentId` at the subscribeEmbeddedPiSession()
call site in attempt.ts
- Wire ctx.params.agentId into the after_tool_call hook context
- Update tests to assert agentId propagation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
(cherry picked from commit aad01edd3e)
* changelog: credit after_tool_call hook contributors
* Update CHANGELOG.md
* agents: preserve adjusted params until tool end
* agents: emit after_tool_call with adjusted args
* tests: cover adjusted after_tool_call params
* tests: align adapter after_tool_call expectation
---------
Co-authored-by: jbeno <jim@jimbeno.net>
Co-authored-by: scoootscooob <zhentongfan@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Fix tool-call lookup failures when models emit whitespace-padded names by normalizing
both transcript history and live streamed embedded-runner tool calls before dispatch.
Co-authored-by: wangchunyue <80630709+openperf@users.noreply.github.com>
Co-authored-by: Sid <sidqin0410@gmail.com>
Co-authored-by: Philipp Spiess <hello@philippspiess.com>
Isolated cron sessions (agentTurn) were grouped with subagent sessions
under the "minimal" prompt mode, which causes buildSkillsSection to
return an empty array. This meant <available_skills> was never included
in the system prompt for isolated cron runs.
Subagent sessions legitimately need minimal prompts (reduced context),
but isolated cron sessions are full agent turns that should have access
to all configured skills, matching the behavior of normal chat sessions
and non-isolated cron runs.
Remove isCronSessionKey from the minimal prompt condition so only
subagent sessions use "minimal" mode.
Fixes openclaw#24888
Co-authored-by: Cursor <cursoragent@cursor.com>
* Telegram: soft-fail reactions and fallback to inbound message id
* Telegram: soft-fail missing reaction message id
* Update CHANGELOG.md
---------
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* fix: sanitize tool call IDs in agent loop for Mistral strict9 format (#23595)
Mistral requires tool call IDs to be exactly 9 alphanumeric characters
([a-zA-Z0-9]{9}). The existing sanitizeToolCallIdsForCloudCodeAssist
mechanism only ran on historical messages at attempt start via
sanitizeSessionHistory, but the pi-agent-core agent loop's internal
tool call → tool result cycles bypassed that path entirely.
Changes:
- Wrap streamFn (like dropThinkingBlocks) so every outbound request
sees sanitized tool call IDs when the transcript policy requires it
- Replace call_${Date.now()} in pendingToolCalls with a 9-char hex ID
generated from crypto.randomBytes
- Add Mistral tool call ID error pattern to ERROR_PATTERNS.format so
the error is correctly classified for retry/rotation
* Changelog: document Mistral strict9 tool-call ID fix
---------
Co-authored-by: echoVic <AkiraVic@outlook.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>