mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 21:07:01 -06:00
refactor: move session/transcript runtime state to SQLite
Migrate OpenClaw session, transcript, and runtime state off per-file .jsonl/JSON storage onto the SQLite state and agent databases, brought up to date with current main. Channel plugins persist via the plugin-state SDK keyed-store seam rather than the raw DB; storage-heavy plugins keep their own SQLite stores via SDK path helpers.
This commit is contained in:
@@ -256,7 +256,7 @@ listed here.
|
||||
|
||||
| # | Hook | What it does | When to use |
|
||||
| --- | --------------------------------- | -------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 1 | `catalog` | Publish provider config into `models.providers` during `models.json` generation | Provider owns a catalog or base URL defaults |
|
||||
| 1 | `catalog` | Publish provider config into `models.providers` during model catalog materialization | Provider owns a catalog or base URL defaults |
|
||||
| 2 | `applyConfigDefaults` | Apply provider-owned global config defaults during config materialization | Defaults depend on auth mode, env, or provider model-family semantics |
|
||||
| -- | _(built-in model lookup)_ | OpenClaw tries the normal registry/catalog path first | _(not a plugin hook)_ |
|
||||
| 3 | `normalizeModelId` | Normalize legacy or preview model-id aliases before lookup | Provider owns alias cleanup before canonical model resolution |
|
||||
@@ -1014,10 +1014,11 @@ plugin index entry with `source: "path"` and a workspace-relative
|
||||
`plugins.load.paths`; the install record avoids duplicating local workstation
|
||||
paths into long-lived config. This keeps local development installs visible to
|
||||
source-plane diagnostics without adding a second raw filesystem-path disclosure
|
||||
surface. The persisted `plugins/installs.json` plugin index is the install
|
||||
source of truth and can be refreshed without loading plugin runtime modules.
|
||||
Its `installRecords` map is durable even when a plugin manifest is missing or
|
||||
invalid; its `plugins` array is a rebuildable manifest view.
|
||||
surface. The persisted plugin index in the global SQLite
|
||||
`installed_plugin_index` table is the install source of truth and can
|
||||
be refreshed without loading plugin runtime modules. Its `installRecords` map is
|
||||
durable even when a plugin manifest is missing or invalid; its `plugins` array
|
||||
is a rebuildable manifest view.
|
||||
|
||||
## Context engine plugins
|
||||
|
||||
|
||||
@@ -17,8 +17,8 @@ Codex mode is not OpenClaw with a different model call underneath. Codex owns mo
|
||||
the native model loop, and OpenClaw adapts its plugin, tool, session, and
|
||||
diagnostic surfaces around that boundary.
|
||||
|
||||
OpenClaw still owns channel routing, session files, visible message delivery,
|
||||
OpenClaw dynamic tools, approvals, media delivery, and a transcript mirror.
|
||||
OpenClaw still owns channel routing, SQLite session state, visible message
|
||||
delivery, OpenClaw dynamic tools, approvals, media delivery, and a transcript mirror.
|
||||
Codex owns the canonical native thread, native model loop, native tool
|
||||
continuation, and native compaction.
|
||||
|
||||
|
||||
@@ -12,9 +12,9 @@ through Codex app-server instead of the built-in OpenClaw harness.
|
||||
|
||||
Use the Codex harness when you want Codex to own the low-level agent session:
|
||||
native thread resume, native tool continuation, native compaction, and
|
||||
app-server execution. OpenClaw still owns chat channels, session files, model
|
||||
selection, OpenClaw dynamic tools, approvals, media delivery, and the visible
|
||||
transcript mirror.
|
||||
app-server execution. OpenClaw still owns chat channels, session state,
|
||||
transcript mirroring, model selection, OpenClaw dynamic tools, approvals, media
|
||||
delivery, and the visible transcript mirror.
|
||||
|
||||
The normal setup uses canonical OpenAI model refs such as `openai/gpt-5.5`.
|
||||
Do not configure legacy Codex GPT refs. Put OpenAI agent auth order
|
||||
@@ -633,7 +633,7 @@ Minimal migrated config:
|
||||
config: {
|
||||
codexPlugins: {
|
||||
enabled: true,
|
||||
allow_destructive_actions: true,
|
||||
allow_destructive_actions: false,
|
||||
plugins: {
|
||||
"google-calendar": {
|
||||
enabled: true,
|
||||
|
||||
@@ -259,8 +259,9 @@ Agents also get LanceDB memory tools from the active memory plugin:
|
||||
|
||||
## Storage
|
||||
|
||||
By default, LanceDB data lives under `~/.openclaw/memory/lancedb`. Override the
|
||||
path with `dbPath`:
|
||||
`memory-lancedb` uses an explicit external LanceDB path. OpenClaw no longer
|
||||
creates a managed `~/.openclaw/memory/lancedb` directory by default; configure
|
||||
`dbPath` when you select this plugin:
|
||||
|
||||
```json5
|
||||
{
|
||||
@@ -269,7 +270,7 @@ path with `dbPath`:
|
||||
"memory-lancedb": {
|
||||
enabled: true,
|
||||
config: {
|
||||
dbPath: "~/.openclaw/memory/lancedb",
|
||||
dbPath: "~/memory/lancedb",
|
||||
embedding: {
|
||||
apiKey: "${OPENAI_API_KEY}",
|
||||
model: "text-embedding-3-small",
|
||||
|
||||
@@ -236,14 +236,9 @@ claims:
|
||||
|
||||
## Compile pipeline
|
||||
|
||||
The compile step reads wiki pages, normalizes summaries, and emits stable
|
||||
machine-facing artifacts under:
|
||||
|
||||
- `.openclaw-wiki/cache/agent-digest.json`
|
||||
- `.openclaw-wiki/cache/claims.jsonl`
|
||||
|
||||
These digests exist so agents and runtime code do not have to scrape Markdown
|
||||
pages.
|
||||
The compile step reads wiki pages, normalizes summaries, and stores stable
|
||||
machine-facing digests in SQLite plugin state. These digests exist so agents
|
||||
and runtime code do not have to scrape Markdown pages.
|
||||
|
||||
Compiled output also powers:
|
||||
|
||||
@@ -353,7 +348,7 @@ plugin supports corpus selection.
|
||||
## Prompt and context behavior
|
||||
|
||||
When `context.includeCompiledDigestPrompt` is enabled, memory prompt sections
|
||||
append a compact compiled snapshot from `agent-digest.json`.
|
||||
append a compact compiled snapshot from SQLite plugin state.
|
||||
|
||||
That snapshot is intentionally small and high-signal:
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ Concrete examples:
|
||||
# Is the GitHub plugin enabled in this config?
|
||||
openclaw path resolve 'oc://config.jsonc/plugins/github/enabled' --json
|
||||
|
||||
# Which tool-call names appear in this session log?
|
||||
# Which tool-call names appear in this exported JSONL trace?
|
||||
openclaw path find 'oc://session.jsonl/[event=tool_call]/name' --json
|
||||
|
||||
# What bytes would this tiny config edit write?
|
||||
|
||||
@@ -17,3 +17,7 @@ Experimental Canvas control and A2UI rendering surfaces for paired nodes.
|
||||
## Surface
|
||||
|
||||
contracts: tools
|
||||
|
||||
Managed Canvas documents are stored in SQLite plugin blob rows. Set
|
||||
`plugins.entries.canvas.config.host.root` only when you intentionally want the
|
||||
host to serve operator-managed files from a directory.
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
---
|
||||
summary: "Captures repeatable workflows as workspace skills, with pending review, safe writes, and skill prompt refresh."
|
||||
read_when:
|
||||
- You are installing, configuring, or auditing the skill-workshop plugin
|
||||
title: "Skill Workshop plugin"
|
||||
---
|
||||
|
||||
# Skill Workshop plugin
|
||||
|
||||
Captures repeatable workflows as workspace skills, with pending review, safe writes, and skill prompt refresh.
|
||||
|
||||
## Distribution
|
||||
|
||||
- Package: `@openclaw/skill-workshop`
|
||||
- Install route: included in OpenClaw
|
||||
|
||||
## Surface
|
||||
|
||||
contracts: tools
|
||||
|
||||
## Related docs
|
||||
|
||||
- [skill-workshop](/plugins/skill-workshop)
|
||||
@@ -38,7 +38,7 @@ Before a harness is selected, OpenClaw has already resolved:
|
||||
- provider and model
|
||||
- runtime auth state
|
||||
- thinking level and context budget
|
||||
- the OpenClaw transcript/session file
|
||||
- the OpenClaw session scope and SQLite transcript rows
|
||||
- workspace, sandbox, and tool policy
|
||||
- channel reply callbacks and streaming callbacks
|
||||
- model fallback and live model switching policy
|
||||
|
||||
@@ -5,5 +5,578 @@ title: "Channel turn"
|
||||
|
||||
This page moved to [Channel inbound API](/plugins/sdk-channel-inbound).
|
||||
|
||||
The old channel-turn runtime aliases were removed. Plugin code should use
|
||||
`runtime.channel.inbound.*`, `channel-inbound`, and `channel-outbound`.
|
||||
Use this when your plugin is on the inbound message hot path. For non-message events (slash commands, modals, button interactions, lifecycle events, reactions, voice state), keep them plugin-local. The kernel only owns events that may become an agent text turn.
|
||||
|
||||
<Info>
|
||||
The kernel is reached through the injected plugin runtime as `runtime.channel.turn.*`. The plugin runtime type is exported from `openclaw/plugin-sdk/core`, so third-party native plugins can use these entry points the same way bundled channel plugins do.
|
||||
</Info>
|
||||
|
||||
## Why a shared kernel
|
||||
|
||||
Channel plugins repeat the same inbound flow: normalize, route, gate, build a context, record session metadata, dispatch the agent turn, finalize delivery state. Without a shared kernel, a change to mention gating, tool-only visible replies, session metadata, pending history, or dispatch finalization has to be applied per channel.
|
||||
|
||||
The kernel keeps four concepts deliberately separate:
|
||||
|
||||
- `ConversationFacts`: where the message came from
|
||||
- `RouteFacts`: which agent and session should process it
|
||||
- `ReplyPlanFacts`: where visible replies should go
|
||||
- `MessageFacts`: what body and supplemental context the agent should see
|
||||
|
||||
Slack DMs, Telegram topics, Matrix threads, and Feishu topic sessions all distinguish these in practice. Treating them as one identifier causes drift over time.
|
||||
|
||||
## Stage lifecycle
|
||||
|
||||
The kernel runs the same fixed pipeline regardless of channel:
|
||||
|
||||
1. `ingest` -- adapter converts a raw platform event into `NormalizedTurnInput`
|
||||
2. `classify` -- adapter declares whether this event can start an agent turn
|
||||
3. `preflight` -- adapter does dedupe, self-echo, hydration, debounce, decryption, partial fact prefill
|
||||
4. `resolve` -- adapter returns a fully assembled turn (route, reply plan, message, delivery)
|
||||
5. `authorize` -- DM, group, mention, and command policy applied to the assembled facts
|
||||
6. `assemble` -- `FinalizedMsgContext` built from the facts via `buildContext`
|
||||
7. `record` -- inbound session metadata and last route persisted
|
||||
8. `dispatch` -- agent turn executed through the buffered block dispatcher
|
||||
9. `finalize` -- adapter `onFinalize` runs even on dispatch error
|
||||
|
||||
Each stage emits a structured log event when a `log` callback is supplied. See [Observability](#observability).
|
||||
|
||||
## Admission kinds
|
||||
|
||||
The kernel does not throw when a turn is gated. It returns a `ChannelTurnAdmission`:
|
||||
|
||||
| Kind | When |
|
||||
| ------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `dispatch` | Turn is admitted. Agent turn runs and the visible reply path is exercised. |
|
||||
| `observeOnly` | Turn runs end-to-end but the delivery adapter sends nothing visible. Used for broadcast observer agents and other passive multi-agent flows. |
|
||||
| `handled` | A platform event was consumed locally (lifecycle, reaction, button, modal). Kernel skips dispatch. |
|
||||
| `drop` | Skip path. Optionally `recordHistory: true` keeps the message in pending group history so a future mention has context. |
|
||||
|
||||
Admission can come from `classify` (event class said it cannot start a turn), from `preflight` (dedupe, self-echo, missing mention with history record), or from `resolveTurn` itself.
|
||||
|
||||
## Entry points
|
||||
|
||||
The runtime exposes three preferred entry points so adapters can opt in at the level that matches the channel.
|
||||
|
||||
```typescript
|
||||
runtime.channel.turn.run(...) // adapter-driven full pipeline
|
||||
runtime.channel.turn.runAssembled(...) // already-built context + delivery adapter
|
||||
runtime.channel.turn.runPrepared(...) // channel owns dispatch; kernel runs record + finalize
|
||||
runtime.channel.turn.buildContext(...) // pure facts to FinalizedMsgContext mapping
|
||||
```
|
||||
|
||||
Two older runtime helpers remain available for Plugin SDK compatibility:
|
||||
|
||||
```typescript
|
||||
runtime.channel.turn.runResolved(...) // deprecated compatibility alias; prefer run
|
||||
runtime.channel.turn.dispatchAssembled(...) // deprecated compatibility alias; prefer runAssembled
|
||||
```
|
||||
|
||||
### run
|
||||
|
||||
Use when your channel can express its inbound flow as a `ChannelTurnAdapter<TRaw>`. The adapter has callbacks for `ingest`, optional `classify`, optional `preflight`, mandatory `resolveTurn`, and optional `onFinalize`.
|
||||
|
||||
```typescript
|
||||
await runtime.channel.turn.run({
|
||||
channel: "tlon",
|
||||
accountId,
|
||||
raw: platformEvent,
|
||||
adapter: {
|
||||
ingest(raw) {
|
||||
return {
|
||||
id: raw.messageId,
|
||||
timestamp: raw.timestamp,
|
||||
rawText: raw.body,
|
||||
textForAgent: raw.body,
|
||||
};
|
||||
},
|
||||
classify(input) {
|
||||
return { kind: "message", canStartAgentTurn: input.rawText.length > 0 };
|
||||
},
|
||||
async preflight(input, eventClass) {
|
||||
if (await isDuplicate(input.id)) {
|
||||
return { admission: { kind: "drop", reason: "dedupe" } };
|
||||
}
|
||||
return {};
|
||||
},
|
||||
resolveTurn(input) {
|
||||
return buildAssembledTurn(input);
|
||||
},
|
||||
onFinalize(result) {
|
||||
clearPendingGroupHistory(result);
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
`run` is the right shape when the channel has small adapter logic and benefits from owning the lifecycle through hooks.
|
||||
|
||||
### runAssembled
|
||||
|
||||
Use when the channel has already resolved routing, built a `FinalizedMsgContext`,
|
||||
and only needs the shared record, reply-pipeline, dispatch, and finalize
|
||||
ordering. This is the preferred shape for simple bundled inbound paths that
|
||||
would otherwise repeat `createChannelMessageReplyPipeline(...)` and
|
||||
`runPrepared(...)` boilerplate.
|
||||
|
||||
```typescript
|
||||
await runtime.channel.turn.runAssembled({
|
||||
cfg,
|
||||
channel: "irc",
|
||||
accountId,
|
||||
agentId: route.agentId,
|
||||
routeSessionKey: route.sessionKey,
|
||||
ctxPayload,
|
||||
recordInboundSession: runtime.channel.session.recordInboundSession,
|
||||
dispatchReplyWithBufferedBlockDispatcher:
|
||||
runtime.channel.reply.dispatchReplyWithBufferedBlockDispatcher,
|
||||
delivery: {
|
||||
deliver: async (payload) => {
|
||||
await sendPlatformReply(payload);
|
||||
},
|
||||
onError: (err, info) => {
|
||||
runtime.error?.(`reply ${info.kind} failed: ${String(err)}`);
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Choose `runAssembled` over `runPrepared` when the only channel-owned dispatch
|
||||
behavior is final payload delivery plus optional typing, reply options, durable
|
||||
delivery, or error logging.
|
||||
|
||||
### runPrepared
|
||||
|
||||
Use when the channel has a complex local dispatcher with previews, retries, edits, or thread bootstrap that must stay channel-owned. The kernel still records the inbound session before dispatch and surfaces a uniform `DispatchedChannelTurnResult`.
|
||||
|
||||
```typescript
|
||||
const { dispatchResult } = await runtime.channel.turn.runPrepared({
|
||||
channel: "matrix",
|
||||
accountId,
|
||||
routeSessionKey,
|
||||
ctxPayload,
|
||||
recordInboundSession,
|
||||
record: {
|
||||
onRecordError,
|
||||
updateLastRoute,
|
||||
},
|
||||
onPreDispatchFailure: async (err) => {
|
||||
await stopStatusReactions();
|
||||
},
|
||||
runDispatch: async () => {
|
||||
return await runMatrixOwnedDispatcher();
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Rich channels (Matrix, Mattermost, Microsoft Teams, Feishu, QQ Bot) use `runPrepared` because their dispatcher orchestrates platform-specific behavior the kernel must not learn about.
|
||||
|
||||
### buildContext
|
||||
|
||||
A pure function that maps fact bundles into `FinalizedMsgContext`. Use it when your channel hand-rolls part of the pipeline but wants consistent context shape.
|
||||
|
||||
```typescript
|
||||
const ctxPayload = runtime.channel.turn.buildContext({
|
||||
channel: "googlechat",
|
||||
accountId,
|
||||
messageId,
|
||||
timestamp,
|
||||
from,
|
||||
sender,
|
||||
conversation,
|
||||
route,
|
||||
reply,
|
||||
message,
|
||||
access,
|
||||
media,
|
||||
supplemental,
|
||||
});
|
||||
```
|
||||
|
||||
`buildContext` is also useful inside `resolveTurn` callbacks when assembling a turn for `run`.
|
||||
|
||||
<Note>
|
||||
Deprecated SDK helpers such as `dispatchInboundReplyWithBase` still bridge through an assembled-turn helper. New plugin code should use `run` or `runPrepared`.
|
||||
</Note>
|
||||
|
||||
## Fact types
|
||||
|
||||
The facts the kernel consumes from your adapter are platform-agnostic. Translate platform objects into these shapes before handing them to the kernel.
|
||||
|
||||
### NormalizedTurnInput
|
||||
|
||||
| Field | Purpose |
|
||||
| ----------------- | ---------------------------------------------------------------------------- |
|
||||
| `id` | Stable message id used for dedupe and logs |
|
||||
| `timestamp` | Optional epoch ms |
|
||||
| `rawText` | Body as received from the platform |
|
||||
| `textForAgent` | Optional cleaned body for the agent (mention strip, typing trim) |
|
||||
| `textForCommands` | Optional body used for `/command` parsing |
|
||||
| `raw` | Optional pass-through reference for adapter callbacks that need the original |
|
||||
|
||||
### ChannelEventClass
|
||||
|
||||
| Field | Purpose |
|
||||
| ---------------------- | ----------------------------------------------------------------------- |
|
||||
| `kind` | `message`, `command`, `interaction`, `reaction`, `lifecycle`, `unknown` |
|
||||
| `canStartAgentTurn` | If false the kernel returns `{ kind: "handled" }` |
|
||||
| `requiresImmediateAck` | Hint for adapters that need to ACK before dispatch |
|
||||
|
||||
### SenderFacts
|
||||
|
||||
| Field | Purpose |
|
||||
| -------------- | -------------------------------------------------------------- |
|
||||
| `id` | Stable platform sender id |
|
||||
| `name` | Display name |
|
||||
| `username` | Handle if distinct from `name` |
|
||||
| `tag` | Discord-style discriminator or platform tag |
|
||||
| `roles` | Role ids, used for member-role allowlist matching |
|
||||
| `isBot` | True when the sender is a known bot (kernel uses for dropping) |
|
||||
| `isSelf` | True when the sender is the configured agent itself |
|
||||
| `displayLabel` | Pre-rendered label for envelope text |
|
||||
|
||||
### ConversationFacts
|
||||
|
||||
| Field | Purpose |
|
||||
| ----------------- | -------------------------------------------------------------------- |
|
||||
| `kind` | `direct`, `group`, or `channel` |
|
||||
| `id` | Conversation id used for routing |
|
||||
| `label` | Human label for the envelope |
|
||||
| `spaceId` | Optional outer space identifier (Slack workspace, Matrix homeserver) |
|
||||
| `parentId` | Outer conversation id when this is a thread |
|
||||
| `threadId` | Thread id when this message is inside a thread |
|
||||
| `nativeChannelId` | Platform-native channel id when different from the routing id |
|
||||
| `routePeer` | Peer used for `resolveAgentRoute` lookup |
|
||||
|
||||
### RouteFacts
|
||||
|
||||
| Field | Purpose |
|
||||
| ----------------------- | ---------------------------------------------------------- |
|
||||
| `agentId` | Agent that should handle this turn |
|
||||
| `accountId` | Optional override (multi-account channels) |
|
||||
| `routeSessionKey` | Session key used for routing |
|
||||
| `dispatchSessionKey` | Session key used at dispatch when different from route key |
|
||||
| `persistedSessionKey` | Session key written to persisted session metadata |
|
||||
| `parentSessionKey` | Parent for branched/threaded sessions |
|
||||
| `modelParentSessionKey` | Model-side parent for branched sessions |
|
||||
| `mainSessionKey` | Main DM owner pin for direct conversations |
|
||||
| `createIfMissing` | Allow record step to create a missing session row |
|
||||
|
||||
### ReplyPlanFacts
|
||||
|
||||
| Field | Purpose |
|
||||
| ------------------------- | ------------------------------------------------------- |
|
||||
| `to` | Logical reply target written into context `To` |
|
||||
| `originatingTo` | Originating context target (`OriginatingTo`) |
|
||||
| `nativeChannelId` | Platform-native channel id for delivery |
|
||||
| `replyTarget` | Final visible-reply destination if it differs from `to` |
|
||||
| `deliveryTarget` | Lower-level delivery override |
|
||||
| `replyToId` | Quoted/anchored message id |
|
||||
| `replyToIdFull` | Full-form quoted id when the platform has both |
|
||||
| `messageThreadId` | Thread id at delivery time |
|
||||
| `threadParentId` | Parent message id of the thread |
|
||||
| `sourceReplyDeliveryMode` | `thread`, `reply`, `channel`, `direct`, or `none` |
|
||||
|
||||
### AccessFacts
|
||||
|
||||
`AccessFacts` carries the booleans the authorize stage needs. Identity matching stays in the channel: the kernel only consumes the result.
|
||||
|
||||
| Field | Purpose |
|
||||
| ---------- | ------------------------------------------------------------------------- |
|
||||
| `dm` | DM allow/pairing/deny decision and `allowFrom` list |
|
||||
| `group` | Group policy, route allow, sender allow, allowlist, mention requirement |
|
||||
| `commands` | Command authorization across configured authorizers |
|
||||
| `mentions` | Whether mention detection is possible and whether the agent was mentioned |
|
||||
|
||||
### MessageFacts
|
||||
|
||||
| Field | Purpose |
|
||||
| ---------------- | -------------------------------------------------------------- |
|
||||
| `body` | Final envelope body (formatted) |
|
||||
| `rawBody` | Raw inbound body |
|
||||
| `bodyForAgent` | Body the agent sees |
|
||||
| `commandBody` | Body used for command parsing |
|
||||
| `envelopeFrom` | Pre-rendered sender label for the envelope |
|
||||
| `senderLabel` | Optional override for the rendered sender |
|
||||
| `preview` | Short redacted preview for logs |
|
||||
| `inboundHistory` | Recent inbound history entries when the channel keeps a buffer |
|
||||
|
||||
### SupplementalContextFacts
|
||||
|
||||
Supplemental context covers quote, forwarded, and thread-bootstrap context. The kernel applies the configured `contextVisibility` policy. The channel adapter only provides facts and `senderAllowed` flags so cross-channel policy stays consistent.
|
||||
|
||||
For group-level prompt context, choose the field by provenance:
|
||||
|
||||
| Field | Use for | Prompt handling |
|
||||
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `groupSystemPrompt` | Trusted operator-authored instructions from plugin config, operator-set group config, or authenticated runtime config | Enters `GroupSystemPrompt` as system prompt material. Core normalizes actual newline characters and preserves system-like markers such as `System:` or `[Assistant]`. |
|
||||
| `untrustedGroupSystemPrompt` | Prompt-like group metadata that end users can influence, such as room names, topics, labels, or dynamic channel metadata | Core sanitizes spoofed system markers and routes the text into `UntrustedStructuredContext` with type `group_prompt_context`; it does not enter `GroupSystemPrompt`. |
|
||||
|
||||
Never copy user-controlled text into `groupSystemPrompt`. Use `untrustedGroupSystemPrompt` when the text could be changed by channel members or other untrusted actors.
|
||||
|
||||
### InboundMediaFacts
|
||||
|
||||
Media is fact-shaped. Platform download, auth, SSRF policy, CDN rules, and decryption stay channel-local. The kernel maps facts into `MediaPath`, `MediaUrl`, `MediaType`, `MediaPaths`, `MediaUrls`, `MediaTypes`, and `MediaTranscribedIndexes`.
|
||||
|
||||
Use `toInboundMediaFacts(...)` from `openclaw/plugin-sdk/channel-inbound` when
|
||||
your channel has a resolved media list and only needs to attach generic facts:
|
||||
|
||||
```typescript
|
||||
media: toInboundMediaFacts(resolvedMedia, {
|
||||
kind: "image",
|
||||
messageId: input.id,
|
||||
});
|
||||
```
|
||||
|
||||
If media mixes local files and URL-only entries, keep the list as media facts.
|
||||
Core preserves array indexes when it writes legacy context fields so downstream
|
||||
media understanding, transcription markers, and prompt notes continue to refer
|
||||
to the same attachment.
|
||||
|
||||
For skipped group messages that should be available to a later mention, pass
|
||||
media facts through the turn `preflight.media` field. The kernel converts those
|
||||
facts into bounded history media entries before recording:
|
||||
|
||||
```typescript
|
||||
preflight(input) {
|
||||
return {
|
||||
admission: { kind: "drop", reason: "missing_mention", recordHistory: true },
|
||||
media: () => toInboundMediaFacts(resolveLocalImages(input), {
|
||||
kind: "image",
|
||||
messageId: input.id,
|
||||
}),
|
||||
history: {
|
||||
key: historyKey,
|
||||
limit: historyLimit,
|
||||
mediaLimit: 4,
|
||||
shouldRecord: () => stillCurrent(input),
|
||||
},
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
History media is intentionally conservative: image-only today, local readable
|
||||
paths only, bounded by the configured media limit, and still tied to the
|
||||
channel history key. Authenticated provider URLs should be downloaded by the
|
||||
plugin before they become model-visible media.
|
||||
|
||||
## History windows
|
||||
|
||||
Message-turn code should use `createChannelHistoryWindow(...)` instead of
|
||||
calling low-level `reply-history` map helpers directly. The old map helpers
|
||||
remain importable as deprecated compatibility exports, but new plugin runtime
|
||||
code should not call them. The window facade keeps text context, structured
|
||||
`InboundHistory`, history-media normalization, and clearing behind one
|
||||
core-owned API while still letting the channel choose how a history line is
|
||||
rendered.
|
||||
|
||||
```typescript
|
||||
const history = createChannelHistoryWindow({ historyMap: groupHistories });
|
||||
|
||||
await history.recordWithMedia({
|
||||
historyKey,
|
||||
limit: historyLimit,
|
||||
entry,
|
||||
media: () =>
|
||||
toInboundMediaFacts(resolvedImages, {
|
||||
kind: "image",
|
||||
messageId: entry.messageId,
|
||||
}),
|
||||
});
|
||||
|
||||
const combinedBody = history.buildPendingContext({
|
||||
historyKey,
|
||||
limit: historyLimit,
|
||||
currentMessage,
|
||||
formatEntry: (entry) => `${entry.sender}: ${entry.body}`,
|
||||
});
|
||||
```
|
||||
|
||||
The older `buildPendingHistoryContextFromMap`,
|
||||
`buildInboundHistoryFromMap`, `recordPendingHistoryEntry*`, and
|
||||
`clearHistoryEntries*` exports remain as deprecated compatibility for plugins
|
||||
that have not migrated yet. New channel work should use the window or the turn
|
||||
kernel record/finalize options.
|
||||
|
||||
## Common message patterns
|
||||
|
||||
Text-only group with mention required:
|
||||
|
||||
```typescript
|
||||
preflight(input) {
|
||||
const decision = resolveInboundMentionDecision({ facts, policy });
|
||||
if (decision.shouldSkip) {
|
||||
return {
|
||||
admission: { kind: "drop", reason: "missing_mention", recordHistory: true },
|
||||
history: { key: historyKey, limit: historyLimit },
|
||||
};
|
||||
}
|
||||
return { access: { mentions: decision } };
|
||||
}
|
||||
```
|
||||
|
||||
Image-only message followed by a later mention:
|
||||
|
||||
```typescript
|
||||
preflight(input) {
|
||||
if (!wasMentioned && resolvedImages.length > 0) {
|
||||
return {
|
||||
admission: { kind: "drop", reason: "missing_mention", recordHistory: true },
|
||||
media: () => toInboundMediaFacts(resolvedImages, {
|
||||
kind: "image",
|
||||
messageId: input.id,
|
||||
}),
|
||||
history: { key: historyKey, limit: historyLimit, mediaLimit: 4 },
|
||||
};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
```
|
||||
|
||||
Explicit reply-to-image:
|
||||
|
||||
```typescript
|
||||
resolveTurn(input, _eventClass, preflight) {
|
||||
return {
|
||||
...assembled,
|
||||
media: toInboundMediaFacts([...currentMedia, ...referencedReplyMedia]),
|
||||
supplemental: {
|
||||
quote: preflight.supplemental?.quote,
|
||||
},
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
Direct message with history:
|
||||
|
||||
```typescript
|
||||
resolveTurn(input) {
|
||||
return {
|
||||
...assembled,
|
||||
history: undefined,
|
||||
message: {
|
||||
rawBody: input.rawText,
|
||||
bodyForAgent: input.textForAgent,
|
||||
},
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Adapter contract
|
||||
|
||||
For full `run`, the adapter shape is:
|
||||
|
||||
```typescript
|
||||
type ChannelTurnAdapter<TRaw> = {
|
||||
ingest(raw: TRaw): Promise<NormalizedTurnInput | null> | NormalizedTurnInput | null;
|
||||
classify?(input: NormalizedTurnInput): Promise<ChannelEventClass> | ChannelEventClass;
|
||||
preflight?(
|
||||
input: NormalizedTurnInput,
|
||||
eventClass: ChannelEventClass,
|
||||
): Promise<PreflightFacts | ChannelTurnAdmission | null | undefined>;
|
||||
resolveTurn(
|
||||
input: NormalizedTurnInput,
|
||||
eventClass: ChannelEventClass,
|
||||
preflight: PreflightFacts,
|
||||
): Promise<ChannelTurnResolved> | ChannelTurnResolved;
|
||||
onFinalize?(result: ChannelTurnResult): Promise<void> | void;
|
||||
};
|
||||
```
|
||||
|
||||
`resolveTurn` returns a `ChannelTurnResolved`, which is an `AssembledChannelTurn` with an optional admission kind. Returning `{ admission: { kind: "observeOnly" } }` runs the turn without producing visible output. The adapter still owns the delivery callback; it just becomes a no-op for that turn.
|
||||
|
||||
`onFinalize` runs on every result, including dispatch errors. Use it to clear pending group history, remove ack reactions, stop status indicators, and flush local state.
|
||||
|
||||
## Delivery adapter
|
||||
|
||||
The kernel does not call the platform directly. The channel hands the kernel a `ChannelEventDeliveryAdapter`:
|
||||
|
||||
```typescript
|
||||
type ChannelEventDeliveryAdapter = {
|
||||
deliver(payload: ReplyPayload, info: ChannelDeliveryInfo): Promise<ChannelDeliveryResult | void>;
|
||||
onError?(err: unknown, info: { kind: string }): void;
|
||||
durable?: false | DurableInboundReplyDeliveryOptions;
|
||||
};
|
||||
|
||||
type ChannelDeliveryResult = {
|
||||
messageIds?: string[];
|
||||
receipt?: MessageReceipt;
|
||||
threadId?: string;
|
||||
replyToId?: string;
|
||||
visibleReplySent?: boolean;
|
||||
};
|
||||
```
|
||||
|
||||
`deliver` is called once per buffered reply chunk. During the message-lifecycle migration, assembled channel-event delivery is channel-owned by default: an omitted `durable` field means the kernel must call `deliver` directly and must not route through generic outbound delivery. Set `durable` only after the channel has been audited to prove the generic send path preserves the old delivery behavior, including reply/thread targets, media handling, sent-message/self-echo caches, status cleanup, and returned message ids. `durable: false` remains a compatibility spelling for "use the channel-owned callback", but unmigrated channels should not need to add it. Return platform message ids when the channel has them so the dispatcher can preserve thread anchors and edit later chunks; newer delivery paths should also return `receipt` so recovery, preview finalization, and duplicate suppression can move off `messageIds`. For observe-only turns, return `{ visibleReplySent: false }` or use `createNoopChannelEventDeliveryAdapter()`.
|
||||
|
||||
Channels using `runPrepared` with a fully channel-owned dispatcher do not have a `ChannelEventDeliveryAdapter`. Those dispatchers are not durable by default. They should keep their direct delivery path until they explicitly opt in to the new send context with a complete target, replay-safe adapter, receipt contract, and channel side-effect hooks.
|
||||
|
||||
Public compatibility helpers such as `recordInboundSessionAndDispatchReply`, `dispatchInboundReplyWithBase`, and direct-DM helpers must stay behavior-preserving during migration. They should not call generic durable delivery before caller-owned `deliver` or `reply` callbacks.
|
||||
|
||||
## Record options
|
||||
|
||||
The record stage wraps `recordInboundSession`. Most channels can use the defaults. Override via `record`:
|
||||
|
||||
```typescript
|
||||
record: {
|
||||
groupResolution,
|
||||
createIfMissing: true,
|
||||
updateLastRoute,
|
||||
onRecordError: (err) => log.warn("record failed", err),
|
||||
trackSessionMetaTask: (task) => pendingTasks.push(task),
|
||||
}
|
||||
```
|
||||
|
||||
The dispatcher waits for the record stage. If record throws, the kernel runs `onPreDispatchFailure` (when provided to `runPrepared`) and rethrows.
|
||||
|
||||
## Observability
|
||||
|
||||
Each stage emits a structured event when a `log` callback is supplied:
|
||||
|
||||
```typescript
|
||||
await runtime.channel.turn.run({
|
||||
channel: "twitch",
|
||||
accountId,
|
||||
raw,
|
||||
adapter,
|
||||
log: (event) => {
|
||||
runtime.log?.debug?.(`turn.${event.stage}:${event.event}`, {
|
||||
channel: event.channel,
|
||||
accountId: event.accountId,
|
||||
messageId: event.messageId,
|
||||
sessionKey: event.sessionKey,
|
||||
admission: event.admission,
|
||||
reason: event.reason,
|
||||
});
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Logged stages: `ingest`, `classify`, `preflight`, `resolve`, `authorize`, `assemble`, `record`, `dispatch`, `finalize`. Avoid logging raw bodies; use `MessageFacts.preview` for short redacted previews.
|
||||
|
||||
## What stays channel-local
|
||||
|
||||
The kernel owns orchestration. The channel still owns:
|
||||
|
||||
- Platform transports (gateway, REST, websocket, polling, webhooks)
|
||||
- Identity resolution and display-name matching
|
||||
- Native commands, slash commands, autocomplete, modals, buttons, voice state
|
||||
- Card, modal, and adaptive-card rendering
|
||||
- Media auth, CDN rules, encrypted media, transcription
|
||||
- Edit, reaction, redaction, and presence APIs
|
||||
- Backfill and platform-side history fetch
|
||||
- Pairing flows that require platform-specific verification
|
||||
|
||||
If two channels start needing the same helper for one of these, extract a shared SDK helper instead of pushing it into the kernel.
|
||||
|
||||
## Stability
|
||||
|
||||
`runtime.channel.turn.*` is part of the public plugin runtime surface. The fact types (`SenderFacts`, `ConversationFacts`, `RouteFacts`, `ReplyPlanFacts`, `AccessFacts`, `MessageFacts`, `SupplementalContextFacts`, `InboundMediaFacts`) and admission shapes (`ChannelTurnAdmission`, `ChannelEventClass`) are reachable through `PluginRuntime` from `openclaw/plugin-sdk/core`.
|
||||
|
||||
Backward compatibility rules apply: new fact fields are additive, admission kinds are not renamed, and the entry point names stay stable. New channel needs that require a non-additive change must go through the plugin SDK migration process.
|
||||
|
||||
## Related
|
||||
|
||||
- [Message lifecycle refactor](/concepts/message-lifecycle-refactor) for the planned send/receive/live lifecycle that will wrap this kernel
|
||||
- [Building channel plugins](/plugins/sdk-channel-plugins) for the broader channel plugin contract
|
||||
- [Plugin runtime helpers](/plugins/sdk-runtime) for other `runtime.*` surfaces
|
||||
- [Plugin internals](/plugins/architecture-internals) for load pipeline and registry mechanics
|
||||
|
||||
@@ -281,7 +281,7 @@ releases.
|
||||
| Already-loaded config assertions and plugin-entry config lookup | `openclaw/plugin-sdk/plugin-config-runtime` |
|
||||
| Current runtime snapshot reads | `openclaw/plugin-sdk/runtime-config-snapshot` |
|
||||
| Config writes | `openclaw/plugin-sdk/config-mutation` |
|
||||
| Session store helpers | `openclaw/plugin-sdk/session-store-runtime` |
|
||||
| SQLite session row helpers | `openclaw/plugin-sdk/session-store-runtime` |
|
||||
| Markdown table config | `openclaw/plugin-sdk/markdown-table-runtime` |
|
||||
| Group policy runtime helpers | `openclaw/plugin-sdk/runtime-group-policy` |
|
||||
| Secret input resolution | `openclaw/plugin-sdk/secret-input-runtime` |
|
||||
@@ -423,7 +423,7 @@ releases.
|
||||
| `resolveThinkingDefault` | `api.runtime.agent.resolveThinkingDefault` |
|
||||
| `resolveAgentTimeoutMs` | `api.runtime.agent.resolveAgentTimeoutMs` |
|
||||
| `ensureAgentWorkspace` | `api.runtime.agent.ensureAgentWorkspace` |
|
||||
| session store helpers | `api.runtime.agent.session.*` |
|
||||
| SQLite session row helpers | `api.runtime.agent.session.*` |
|
||||
|
||||
</Step>
|
||||
|
||||
@@ -451,7 +451,6 @@ releases.
|
||||
| Bounded async task concurrency | `openclaw/plugin-sdk/concurrency-runtime` |
|
||||
| Numeric coercion | `openclaw/plugin-sdk/number-runtime` |
|
||||
| Process-local async lock | `openclaw/plugin-sdk/async-lock-runtime` |
|
||||
| File locks | `openclaw/plugin-sdk/file-lock` |
|
||||
|
||||
Bundled plugins are scanner-guarded against `infra-runtime`, so repo code
|
||||
cannot regress to the broad barrel.
|
||||
@@ -586,8 +585,9 @@ releases.
|
||||
| `plugin-sdk/reply-history` | Reply-history helpers | `createChannelHistoryWindow`; deprecated map-helper compatibility exports such as `buildPendingHistoryContextFromMap`, `recordPendingHistoryEntry`, and `clearHistoryEntriesIfEnabled` |
|
||||
| `plugin-sdk/reply-reference` | Reply reference planning | `createReplyReferencePlanner` |
|
||||
| `plugin-sdk/reply-chunking` | Reply chunk helpers | Text/markdown chunking helpers |
|
||||
| `plugin-sdk/session-store-runtime` | Session store helpers | Store path + updated-at helpers |
|
||||
| `plugin-sdk/state-paths` | State path helpers | State and OAuth dir helpers |
|
||||
| `plugin-sdk/session-store-runtime` | Session row helpers | SQLite-backed session row, session-key, updated-at, and transcript row helpers |
|
||||
| `plugin-sdk/sqlite-runtime` | SQLite helpers | Focused database open/path helpers for first-party runtime and migration tests |
|
||||
| `plugin-sdk/state-paths` | State path helpers | Config, credentials, migration, and explicit operator-file path helpers; runtime state and caches belong in SQLite stores |
|
||||
| `plugin-sdk/routing` | Routing/session-key helpers | `resolveAgentRoute`, `buildAgentSessionKey`, `resolveDefaultAgentBoundAccountId`, session-key normalization helpers |
|
||||
| `plugin-sdk/status-helpers` | Channel status helpers | Channel/account status summary builders, runtime-state defaults, issue metadata helpers |
|
||||
| `plugin-sdk/target-resolver-runtime` | Target resolver helpers | Shared target resolver helpers |
|
||||
@@ -654,7 +654,8 @@ releases.
|
||||
| `plugin-sdk/memory-core-engine-runtime` | Memory engine runtime facade | Memory index/search runtime facade |
|
||||
| `plugin-sdk/memory-core-host-engine-foundation` | Memory host foundation engine | Memory host foundation engine exports |
|
||||
| `plugin-sdk/memory-core-host-engine-embeddings` | Memory host embedding engine | Memory embedding contracts, registry access, local provider, and generic batch/remote helpers; concrete remote providers live in their owning plugins |
|
||||
| `plugin-sdk/memory-core-host-engine-qmd` | Memory host QMD engine | Memory host QMD engine exports |
|
||||
| `plugin-sdk/memory-core-host-engine-qmd` | Memory host QMD engine | Memory host QMD engine exports; new code should use `memory-core-host-engine-session-transcripts` for SQLite transcript indexing helpers |
|
||||
| `plugin-sdk/memory-core-host-engine-session-transcripts` | Memory host SQLite session transcript engine | Memory host SQLite session transcript indexing exports |
|
||||
| `plugin-sdk/memory-core-host-engine-storage` | Memory host storage engine | Memory host storage engine exports |
|
||||
| `plugin-sdk/memory-core-host-multimodal` | Memory host multimodal helpers | Memory host multimodal helpers |
|
||||
| `plugin-sdk/memory-core-host-query` | Memory host query helpers | Memory host query helpers |
|
||||
|
||||
@@ -150,21 +150,22 @@ two-party event loops that do not go through the shared inbound reply runner.
|
||||
|
||||
`normalizeThinkingLevel(...)` converts user text such as `on`, `x-high`, or `extra high` to the canonical stored level before checking it against the resolved policy.
|
||||
|
||||
**Session store helpers** are under `api.runtime.agent.session`:
|
||||
**SQLite session row helpers** are under `api.runtime.agent.session`:
|
||||
|
||||
```typescript
|
||||
const entry = api.runtime.agent.session.getSessionEntry({ agentId, sessionKey });
|
||||
for (const { sessionKey, entry } of api.runtime.agent.session.listSessionEntries({ agentId })) {
|
||||
// Iterate session rows without depending on the legacy sessions.json shape.
|
||||
}
|
||||
await api.runtime.agent.session.patchSessionEntry({
|
||||
agentId,
|
||||
sessionKey,
|
||||
update: (entry) => ({ thinkingLevel: "high" }),
|
||||
update: (current) => ({
|
||||
...current,
|
||||
thinkingLevel: "high",
|
||||
}),
|
||||
});
|
||||
const filePath = api.runtime.agent.session.resolveSessionFilePath(cfg, sessionId);
|
||||
```
|
||||
|
||||
Prefer `getSessionEntry(...)`, `listSessionEntries(...)`, `patchSessionEntry(...)`, or `upsertSessionEntry(...)` for session workflows. These helpers address sessions by agent/session identity so plugins do not depend on the legacy `sessions.json` storage shape. Use `preserveActivity: true` for metadata-only patches that should not refresh session activity, and `replaceEntry: true` only when the callback returns a complete entry and deleted fields must stay deleted. `loadSessionStore(...)` remains as a deprecated compatibility escape hatch for callers that intentionally need a mutable whole-store clone.
|
||||
Prefer row helpers such as `getSessionEntry(...)`, `listSessionEntries(...)`, `patchSessionEntry(...)`, and `upsertSessionEntry(...)` for runtime writes. They route through the SQLite session row store and preserve concurrent updates. Legacy `sessions.json` parsing belongs in doctor/migration code, not plugin runtime paths.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="api.runtime.agent.defaults">
|
||||
|
||||
@@ -27,6 +27,8 @@ For the plugin authoring guide, see [Plugin SDK overview](/plugins/sdk-overview)
|
||||
| `plugin-sdk/core` | `defineChannelPluginEntry`, `createChatChannelPlugin`, `createChannelPluginBase`, `defineSetupPluginEntry`, `buildChannelConfigSchema`, `buildJsonChannelConfigSchema` |
|
||||
| `plugin-sdk/config-schema` | `OpenClawSchema` |
|
||||
| `plugin-sdk/provider-entry` | `defineSingleProviderPluginEntry` |
|
||||
| `plugin-sdk/provider-ai` | OpenClaw-owned provider stream/model/message types plus simple streaming helpers used by bundled provider plugins |
|
||||
| `plugin-sdk/provider-ai-oauth` | OpenClaw-owned OAuth helper facade for provider runtime code |
|
||||
| `plugin-sdk/migration` | Migration provider item helpers such as `createMigrationItem`, reason constants, item status markers, redaction helpers, and `summarizeMigrationItems` |
|
||||
| `plugin-sdk/migration-runtime` | Runtime migration helpers such as `copyMigrationFileItem`, `withCachedMigrationConfigRuntime`, and `writeMigrationReport` |
|
||||
| `plugin-sdk/health` | Doctor health-check registration, detection, repair, selection, severity, and finding types for bundled health consumers |
|
||||
@@ -237,9 +239,10 @@ and pairing-path families.
|
||||
| `plugin-sdk/reply-history` | Shared short-window reply-history helpers. New message-turn code should use `createChannelHistoryWindow`; lower-level map helpers remain deprecated compatibility exports only |
|
||||
| `plugin-sdk/reply-reference` | `createReplyReferencePlanner` |
|
||||
| `plugin-sdk/reply-chunking` | Narrow text/markdown chunking helpers |
|
||||
| `plugin-sdk/session-store-runtime` | Session workflow helpers (`getSessionEntry`, `listSessionEntries`, `patchSessionEntry`, `upsertSessionEntry`), legacy session store path/session-key helpers, updated-at reads, and deprecated whole-store mutation helpers |
|
||||
| `plugin-sdk/cron-store-runtime` | Cron store path/load/save helpers |
|
||||
| `plugin-sdk/state-paths` | State/OAuth dir path helpers |
|
||||
| `plugin-sdk/session-store-runtime` | SQLite-backed session row, session-key, updated-at, and transcript row helpers |
|
||||
| `plugin-sdk/sqlite-runtime` | Focused SQLite database open/path helpers for first-party runtime and migration tests |
|
||||
| `plugin-sdk/cron-store-runtime` | SQLite cron store load/save helpers |
|
||||
| `plugin-sdk/state-paths` | Config, credentials, migration, and explicit operator-file path helpers; runtime state and caches belong in SQLite stores |
|
||||
| `plugin-sdk/plugin-state-runtime` | Plugin sidecar SQLite keyed-state types |
|
||||
| `plugin-sdk/routing` | Route/session-key/account binding helpers such as `resolveAgentRoute`, `buildAgentSessionKey`, and `resolveDefaultAgentBoundAccountId` |
|
||||
| `plugin-sdk/status-helpers` | Shared channel/account status summary helpers, runtime-state defaults, and issue metadata helpers |
|
||||
@@ -257,10 +260,8 @@ and pairing-path families.
|
||||
| `plugin-sdk/markdown-table-runtime` | Markdown table mode and conversion helpers |
|
||||
| `plugin-sdk/model-session-runtime` | Model/session override helpers such as `applyModelOverrideToSessionEntry` and `resolveAgentMaxConcurrent` |
|
||||
| `plugin-sdk/talk-config-runtime` | Talk provider config resolution helpers |
|
||||
| `plugin-sdk/json-store` | Small JSON state read/write helpers |
|
||||
| `plugin-sdk/json-unsafe-integers` | JSON parsing helpers that preserve unsafe integer literals as strings |
|
||||
| `plugin-sdk/file-lock` | Re-entrant file-lock helpers |
|
||||
| `plugin-sdk/persistent-dedupe` | Disk-backed dedupe cache helpers |
|
||||
| `plugin-sdk/json-store` | External JSON config/import helpers; runtime state and caches belong in SQLite stores |
|
||||
| `plugin-sdk/persistent-dedupe` | SQLite-backed dedupe cache helpers |
|
||||
| `plugin-sdk/acp-runtime` | ACP runtime/session and reply-dispatch helpers |
|
||||
| `plugin-sdk/acp-runtime-backend` | Lightweight ACP backend registration and reply-dispatch helpers for startup-loaded plugins |
|
||||
| `plugin-sdk/acp-binding-resolve-runtime` | Read-only ACP binding resolution without lifecycle startup imports |
|
||||
@@ -274,7 +275,7 @@ and pairing-path families.
|
||||
| `plugin-sdk/native-command-registry` | Native command registry/build/serialize helpers |
|
||||
| `plugin-sdk/agent-harness` | Experimental trusted-plugin surface for low-level agent harnesses: harness types, active-run steer/abort helpers, OpenClaw tool bridge helpers, runtime-plan tool policy helpers, terminal outcome classification, tool progress formatting/detail helpers, and attempt result utilities |
|
||||
| `plugin-sdk/provider-zai-endpoint` | Deprecated Z.AI provider-owned endpoint detection facade; use the Z.AI plugin public API |
|
||||
| `plugin-sdk/async-lock-runtime` | Process-local async lock helper for small runtime state files |
|
||||
| `plugin-sdk/async-lock-runtime` | Process-local async lock helper for small runtime critical sections |
|
||||
| `plugin-sdk/channel-activity-runtime` | Channel activity telemetry helper |
|
||||
| `plugin-sdk/concurrency-runtime` | Bounded async task concurrency helper |
|
||||
| `plugin-sdk/dedupe-runtime` | In-memory dedupe cache helpers |
|
||||
@@ -295,11 +296,13 @@ and pairing-path families.
|
||||
| `plugin-sdk/inline-image-data-url-runtime` | Inline image data URL sanitizer and signature sniffing helpers without the broad media runtime surface |
|
||||
| `plugin-sdk/response-limit-runtime` | Bounded response-body reader without the broad media runtime surface |
|
||||
| `plugin-sdk/session-binding-runtime` | Current conversation binding state without configured binding routing or pairing stores |
|
||||
| `plugin-sdk/session-store-runtime` | Session-store helpers without broad config writes/maintenance imports |
|
||||
| `plugin-sdk/session-store-runtime` | SQLite session row helpers without broad config writes, maintenance imports, or raw database openers |
|
||||
| `plugin-sdk/sqlite-runtime` | Focused SQLite database helpers without session-row helper imports |
|
||||
| `plugin-sdk/context-visibility-runtime` | Context visibility resolution and supplemental context filtering without broad config/security imports |
|
||||
| `plugin-sdk/string-coerce-runtime` | Narrow primitive record/string coercion and normalization helpers without markdown/logging imports |
|
||||
| `plugin-sdk/host-runtime` | Hostname and SCP host normalization helpers |
|
||||
| `plugin-sdk/retry-runtime` | Retry config and retry runner helpers |
|
||||
| `plugin-sdk/agent-core` | OpenClaw-owned agent-loop types such as `AgentMessage`, `AgentEvent`, `AgentTool`, `AgentToolResult`, and `StreamFn` |
|
||||
| `plugin-sdk/agent-runtime` | Agent dir/identity/workspace helpers, including `resolveAgentDir`, `resolveDefaultAgentDir`, and deprecated `resolveOpenClawAgentDir` compatibility export |
|
||||
| `plugin-sdk/directory-runtime` | Config-backed directory query/dedup |
|
||||
| `plugin-sdk/keyed-async-queue` | `KeyedAsyncQueue` |
|
||||
@@ -318,8 +321,7 @@ and pairing-path families.
|
||||
| `plugin-sdk/speech` | Speech provider types plus provider-facing directive, registry, validation, OpenAI-compatible TTS builder, and speech helper exports |
|
||||
| `plugin-sdk/speech-core` | Shared speech provider types, registry, directive, normalization, and speech helper exports |
|
||||
| `plugin-sdk/realtime-transcription` | Realtime transcription provider types, registry helpers, and shared WebSocket session helper |
|
||||
| `plugin-sdk/realtime-bootstrap-context` | Realtime profile bootstrap helper for bounded `IDENTITY.md`, `USER.md`, and `SOUL.md` context injection |
|
||||
| `plugin-sdk/realtime-voice` | Realtime voice provider types, registry helpers, and shared realtime voice behavior helpers, including output activity tracking |
|
||||
| `plugin-sdk/realtime-voice` | Realtime voice provider types and registry helpers |
|
||||
| `plugin-sdk/image-generation` | Image generation provider types plus image asset/data URL helpers and the OpenAI-compatible image provider builder |
|
||||
| `plugin-sdk/image-generation-core` | Shared image-generation types, failover, auth, and registry helpers |
|
||||
| `plugin-sdk/music-generation` | Music generation provider/request/result types |
|
||||
@@ -349,8 +351,9 @@ and pairing-path families.
|
||||
| `plugin-sdk/memory-core` | Bundled memory-core helper surface for manager/config/file/CLI helpers |
|
||||
| `plugin-sdk/memory-core-engine-runtime` | Memory index/search runtime facade |
|
||||
| `plugin-sdk/memory-core-host-engine-foundation` | Memory host foundation engine exports |
|
||||
| `plugin-sdk/memory-core-host-engine-embeddings` | Memory host embedding contracts, registry access, local provider, and generic batch/remote helpers. `registerMemoryEmbeddingProvider` on this surface is deprecated; use the generic embedding provider API for new providers. |
|
||||
| `plugin-sdk/memory-core-host-engine-qmd` | Memory host QMD engine exports |
|
||||
| `plugin-sdk/memory-core-host-engine-embeddings` | Memory host embedding contracts, registry access, local provider, and generic batch/remote helpers |
|
||||
| `plugin-sdk/memory-core-host-engine-qmd` | Memory host QMD engine exports; use `memory-core-host-engine-session-transcripts` for SQLite transcript indexing helpers |
|
||||
| `plugin-sdk/memory-core-host-engine-session-transcripts` | Memory host SQLite session transcript indexing exports |
|
||||
| `plugin-sdk/memory-core-host-engine-storage` | Memory host storage engine exports |
|
||||
| `plugin-sdk/memory-core-host-multimodal` | Memory host multimodal helpers |
|
||||
| `plugin-sdk/memory-core-host-query` | Memory host query helpers |
|
||||
|
||||
@@ -0,0 +1,713 @@
|
||||
---
|
||||
summary: "Experimental capture of reusable procedures as workspace skills with review, approval, quarantine, and hot skill refresh"
|
||||
title: "Skill workshop plugin"
|
||||
read_when:
|
||||
- You want agents to turn corrections or reusable procedures into workspace skills
|
||||
- You are configuring procedural skill memory
|
||||
- You are debugging skill_workshop tool behavior
|
||||
- You are deciding whether to enable automatic skill creation
|
||||
---
|
||||
|
||||
Skill Workshop is **experimental**. It is disabled by default, its capture
|
||||
heuristics and reviewer prompts may change between releases, and automatic
|
||||
writes should be used only in trusted workspaces after reviewing pending-mode
|
||||
output first.
|
||||
|
||||
Skill Workshop is procedural memory for workspace skills. It lets an agent turn
|
||||
reusable workflows, user corrections, hard-won fixes, and recurring pitfalls
|
||||
into `SKILL.md` files under:
|
||||
|
||||
```text
|
||||
<workspace>/skills/<skill-name>/SKILL.md
|
||||
```
|
||||
|
||||
This is different from long-term memory:
|
||||
|
||||
- **Memory** stores facts, preferences, entities, and past context.
|
||||
- **Skills** store reusable procedures the agent should follow on future tasks.
|
||||
- **Skill Workshop** is the bridge from a useful turn to a durable workspace
|
||||
skill, with safety checks and optional approval.
|
||||
|
||||
Skill Workshop is useful when the agent learns a procedure such as:
|
||||
|
||||
- how to validate externally sourced animated GIF assets
|
||||
- how to replace screenshot assets and verify dimensions
|
||||
- how to run a repo-specific QA scenario
|
||||
- how to debug a recurring provider failure
|
||||
- how to repair a stale local workflow note
|
||||
|
||||
It is not intended for:
|
||||
|
||||
- facts like "the user likes blue"
|
||||
- broad autobiographical memory
|
||||
- raw transcript archiving
|
||||
- secrets, credentials, or hidden prompt text
|
||||
- one-off instructions that will not repeat
|
||||
|
||||
## Default state
|
||||
|
||||
The bundled plugin is **experimental** and **disabled by default** unless it is
|
||||
explicitly enabled in `plugins.entries.skill-workshop`.
|
||||
|
||||
The plugin manifest does not set `enabledByDefault: true`. The `enabled: true`
|
||||
default inside the plugin config schema applies only after the plugin entry has
|
||||
already been selected and loaded.
|
||||
|
||||
Experimental means:
|
||||
|
||||
- the plugin is supported enough for opt-in testing and dogfooding
|
||||
- proposal storage, reviewer thresholds, and capture heuristics can evolve
|
||||
- pending approval is the recommended starting mode
|
||||
- auto apply is for trusted personal/workspace setups, not shared or hostile
|
||||
input-heavy environments
|
||||
|
||||
## Enable
|
||||
|
||||
Minimal safe config:
|
||||
|
||||
```json5
|
||||
{
|
||||
plugins: {
|
||||
entries: {
|
||||
"skill-workshop": {
|
||||
enabled: true,
|
||||
config: {
|
||||
autoCapture: true,
|
||||
approvalPolicy: "pending",
|
||||
reviewMode: "hybrid",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
With this config:
|
||||
|
||||
- the `skill_workshop` tool is available
|
||||
- explicit reusable corrections are queued as pending proposals
|
||||
- threshold-based reviewer passes can propose skill updates
|
||||
- no skill file is written until a pending proposal is applied
|
||||
|
||||
Use automatic writes only in trusted workspaces:
|
||||
|
||||
```json5
|
||||
{
|
||||
plugins: {
|
||||
entries: {
|
||||
"skill-workshop": {
|
||||
enabled: true,
|
||||
config: {
|
||||
autoCapture: true,
|
||||
approvalPolicy: "auto",
|
||||
reviewMode: "hybrid",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
`approvalPolicy: "auto"` still uses the same scanner and quarantine path. It
|
||||
does not apply proposals with critical findings.
|
||||
|
||||
## Configuration
|
||||
|
||||
| Key | Default | Range / values | Meaning |
|
||||
| -------------------- | ----------- | ------------------------------------------- | -------------------------------------------------------------------- |
|
||||
| `enabled` | `true` | boolean | Enables the plugin after the plugin entry is loaded. |
|
||||
| `autoCapture` | `true` | boolean | Enables post-turn capture/review on successful agent turns. |
|
||||
| `approvalPolicy` | `"pending"` | `"pending"`, `"auto"` | Queue proposals or write safe proposals automatically. |
|
||||
| `reviewMode` | `"hybrid"` | `"off"`, `"heuristic"`, `"llm"`, `"hybrid"` | Chooses explicit correction capture, LLM reviewer, both, or neither. |
|
||||
| `reviewInterval` | `15` | `1..200` | Run reviewer after this many successful turns. |
|
||||
| `reviewMinToolCalls` | `8` | `1..500` | Run reviewer after this many observed tool calls. |
|
||||
| `reviewTimeoutMs` | `45000` | `5000..180000` | Timeout for the embedded reviewer run. |
|
||||
| `maxPending` | `50` | `1..200` | Max pending/quarantined proposals kept per workspace. |
|
||||
| `maxSkillBytes` | `40000` | `1024..200000` | Max generated skill/support file size. |
|
||||
|
||||
Recommended profiles:
|
||||
|
||||
```json5
|
||||
// Conservative: explicit tool use only, no automatic capture.
|
||||
{
|
||||
autoCapture: false,
|
||||
approvalPolicy: "pending",
|
||||
reviewMode: "off",
|
||||
}
|
||||
```
|
||||
|
||||
```json5
|
||||
// Review-first: capture automatically, but require approval.
|
||||
{
|
||||
autoCapture: true,
|
||||
approvalPolicy: "pending",
|
||||
reviewMode: "hybrid",
|
||||
}
|
||||
```
|
||||
|
||||
```json5
|
||||
// Trusted automation: write safe proposals immediately.
|
||||
{
|
||||
autoCapture: true,
|
||||
approvalPolicy: "auto",
|
||||
reviewMode: "hybrid",
|
||||
}
|
||||
```
|
||||
|
||||
```json5
|
||||
// Low-cost: no reviewer LLM call, only explicit correction phrases.
|
||||
{
|
||||
autoCapture: true,
|
||||
approvalPolicy: "pending",
|
||||
reviewMode: "heuristic",
|
||||
}
|
||||
```
|
||||
|
||||
## Capture paths
|
||||
|
||||
Skill Workshop has three capture paths.
|
||||
|
||||
### Tool suggestions
|
||||
|
||||
The model can call `skill_workshop` directly when it sees a reusable procedure
|
||||
or when the user asks it to save/update a skill.
|
||||
|
||||
This is the most explicit path and works even with `autoCapture: false`.
|
||||
|
||||
### Heuristic capture
|
||||
|
||||
When `autoCapture` is enabled and `reviewMode` is `heuristic` or `hybrid`, the
|
||||
plugin scans successful turns for explicit user correction phrases:
|
||||
|
||||
- `next time`
|
||||
- `from now on`
|
||||
- `remember to`
|
||||
- `make sure to`
|
||||
- `always ... use/check/verify/record/save/prefer`
|
||||
- `prefer ... when/for/instead/use`
|
||||
- `when asked`
|
||||
|
||||
The heuristic creates a proposal from the latest matching user instruction. It
|
||||
uses topic hints to choose skill names for common workflows:
|
||||
|
||||
- animated GIF tasks -> `animated-gif-workflow`
|
||||
- screenshot or asset tasks -> `screenshot-asset-workflow`
|
||||
- QA or scenario tasks -> `qa-scenario-workflow`
|
||||
- GitHub PR tasks -> `github-pr-workflow`
|
||||
- fallback -> `learned-workflows`
|
||||
|
||||
Heuristic capture is intentionally narrow. It is for clear corrections and
|
||||
repeatable process notes, not for general transcript summarization.
|
||||
|
||||
### LLM reviewer
|
||||
|
||||
When `autoCapture` is enabled and `reviewMode` is `llm` or `hybrid`, the plugin
|
||||
runs a compact embedded reviewer after thresholds are reached.
|
||||
|
||||
The reviewer receives:
|
||||
|
||||
- the recent transcript text, capped to the last 12,000 characters
|
||||
- up to 12 existing workspace skills
|
||||
- up to 2,000 characters from each existing skill
|
||||
- JSON-only instructions
|
||||
|
||||
The reviewer has no tools:
|
||||
|
||||
- `disableTools: true`
|
||||
- `toolsAllow: []`
|
||||
- `disableMessageTool: true`
|
||||
|
||||
The reviewer returns either `{ "action": "none" }` or one proposal. The `action` field is `create`, `append`, or `replace` - prefer `append`/`replace` when a relevant skill already exists; use `create` only when no existing skill fits.
|
||||
|
||||
Example `create`:
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "create",
|
||||
"skillName": "media-asset-qa",
|
||||
"title": "Media Asset QA",
|
||||
"reason": "Reusable animated media acceptance workflow",
|
||||
"description": "Validate externally sourced animated media before product use.",
|
||||
"body": "## Workflow\n\n- Verify true animation.\n- Record attribution.\n- Store a local approved copy.\n- Verify in product UI before final reply."
|
||||
}
|
||||
```
|
||||
|
||||
`append` adds `section` + `body`. `replace` swaps `oldText` for `newText` in the named skill.
|
||||
|
||||
## Proposal lifecycle
|
||||
|
||||
Every generated update becomes a proposal with:
|
||||
|
||||
- `id`
|
||||
- `createdAt`
|
||||
- `updatedAt`
|
||||
- `workspaceDir`
|
||||
- optional `agentId`
|
||||
- optional `sessionId`
|
||||
- `skillName`
|
||||
- `title`
|
||||
- `reason`
|
||||
- `source`: `tool`, `agent_end`, or `reviewer`
|
||||
- `status`
|
||||
- `change`
|
||||
- optional `scanFindings`
|
||||
- optional `quarantineReason`
|
||||
|
||||
Proposal statuses:
|
||||
|
||||
- `pending` - waiting for approval
|
||||
- `applied` - written to `<workspace>/skills`
|
||||
- `rejected` - rejected by operator/model
|
||||
- `quarantined` - blocked by critical scanner findings
|
||||
|
||||
State is stored per workspace under the Gateway state directory:
|
||||
|
||||
```text
|
||||
<stateDir>/skill-workshop/<workspace-hash>.json
|
||||
```
|
||||
|
||||
Pending and quarantined proposals are deduplicated by skill name and change
|
||||
payload. The store keeps the newest pending/quarantined proposals up to
|
||||
`maxPending`.
|
||||
|
||||
## Tool reference
|
||||
|
||||
The plugin registers one agent tool:
|
||||
|
||||
```text
|
||||
skill_workshop
|
||||
```
|
||||
|
||||
### `status`
|
||||
|
||||
Count proposals by state for the active workspace.
|
||||
|
||||
```json
|
||||
{ "action": "status" }
|
||||
```
|
||||
|
||||
Result shape:
|
||||
|
||||
```json
|
||||
{
|
||||
"workspaceDir": "/path/to/workspace",
|
||||
"pending": 1,
|
||||
"quarantined": 0,
|
||||
"applied": 3,
|
||||
"rejected": 0
|
||||
}
|
||||
```
|
||||
|
||||
### `list_pending`
|
||||
|
||||
List pending proposals.
|
||||
|
||||
```json
|
||||
{ "action": "list_pending" }
|
||||
```
|
||||
|
||||
To list another status:
|
||||
|
||||
```json
|
||||
{ "action": "list_pending", "status": "applied" }
|
||||
```
|
||||
|
||||
Valid `status` values:
|
||||
|
||||
- `pending`
|
||||
- `applied`
|
||||
- `rejected`
|
||||
- `quarantined`
|
||||
|
||||
### `list_quarantine`
|
||||
|
||||
List quarantined proposals.
|
||||
|
||||
```json
|
||||
{ "action": "list_quarantine" }
|
||||
```
|
||||
|
||||
Use this when automatic capture appears to do nothing and the logs mention
|
||||
`skill-workshop: quarantined <skill>`.
|
||||
|
||||
### `inspect`
|
||||
|
||||
Fetch a proposal by id.
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "inspect",
|
||||
"id": "proposal-id"
|
||||
}
|
||||
```
|
||||
|
||||
### `suggest`
|
||||
|
||||
Create a proposal. With `approvalPolicy: "pending"` (default), this queues instead of writing.
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "suggest",
|
||||
"skillName": "animated-gif-workflow",
|
||||
"title": "Animated GIF Workflow",
|
||||
"reason": "User established reusable GIF validation rules.",
|
||||
"description": "Validate animated GIF assets before using them.",
|
||||
"body": "## Workflow\n\n- Verify the URL resolves to image/gif.\n- Confirm it has multiple frames.\n- Record attribution and license.\n- Avoid hotlinking when a local asset is needed."
|
||||
}
|
||||
```
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="Request immediate write in auto mode (apply: true)">
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "suggest",
|
||||
"apply": true,
|
||||
"skillName": "animated-gif-workflow",
|
||||
"description": "Validate animated GIF assets before using them.",
|
||||
"body": "## Workflow\n\n- Verify true animation.\n- Record attribution."
|
||||
}
|
||||
```
|
||||
|
||||
With `approvalPolicy: "pending"`, `apply: true` still queues the proposal. Review it, then use
|
||||
the `apply` action after approval.
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Force pending under auto policy (apply: false)">
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "suggest",
|
||||
"apply": false,
|
||||
"skillName": "screenshot-asset-workflow",
|
||||
"description": "Screenshot replacement workflow.",
|
||||
"body": "## Workflow\n\n- Verify dimensions.\n- Optimize the PNG.\n- Run the relevant gate."
|
||||
}
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Append to a named section">
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "suggest",
|
||||
"skillName": "qa-scenario-workflow",
|
||||
"section": "Workflow",
|
||||
"description": "QA scenario workflow.",
|
||||
"body": "- For media QA, verify generated assets render and pass final assertions."
|
||||
}
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Replace exact text">
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "suggest",
|
||||
"skillName": "github-pr-workflow",
|
||||
"oldText": "- Check the PR.",
|
||||
"newText": "- Check unresolved review threads, CI status, linked issues, and changed files before deciding."
|
||||
}
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
### `apply`
|
||||
|
||||
Apply a pending proposal.
|
||||
|
||||
With `approvalPolicy: "pending"`, this action asks for operator approval before writing the
|
||||
workspace skill.
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "apply",
|
||||
"id": "proposal-id"
|
||||
}
|
||||
```
|
||||
|
||||
`apply` refuses quarantined proposals:
|
||||
|
||||
```text
|
||||
quarantined proposal cannot be applied
|
||||
```
|
||||
|
||||
### `reject`
|
||||
|
||||
Mark a proposal rejected.
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "reject",
|
||||
"id": "proposal-id"
|
||||
}
|
||||
```
|
||||
|
||||
### `write_support_file`
|
||||
|
||||
Write a supporting file inside an existing or proposed skill directory.
|
||||
|
||||
Allowed top-level support directories:
|
||||
|
||||
- `references/`
|
||||
- `templates/`
|
||||
- `scripts/`
|
||||
- `assets/`
|
||||
|
||||
Example:
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "write_support_file",
|
||||
"skillName": "release-workflow",
|
||||
"relativePath": "references/checklist.md",
|
||||
"body": "# Release Checklist\n\n- Run release docs.\n- Verify changelog.\n"
|
||||
}
|
||||
```
|
||||
|
||||
Support files are workspace-scoped, path-checked, byte-limited by
|
||||
`maxSkillBytes`, scanned, and written atomically.
|
||||
|
||||
## Skill writes
|
||||
|
||||
Skill Workshop writes only under:
|
||||
|
||||
```text
|
||||
<workspace>/skills/<normalized-skill-name>/
|
||||
```
|
||||
|
||||
Skill names are normalized:
|
||||
|
||||
- lowercased
|
||||
- non `[a-z0-9_-]` runs become `-`
|
||||
- leading/trailing non-alphanumerics are removed
|
||||
- max length is 80 characters
|
||||
- final name must match `[a-z0-9][a-z0-9_-]{1,79}`
|
||||
|
||||
For `create`:
|
||||
|
||||
- if the skill does not exist, Skill Workshop writes a new `SKILL.md`
|
||||
- if it already exists, Skill Workshop appends the body to `## Workflow`
|
||||
|
||||
For `append`:
|
||||
|
||||
- if the skill exists, Skill Workshop appends to the requested section
|
||||
- if it does not exist, Skill Workshop creates a minimal skill then appends
|
||||
|
||||
For `replace`:
|
||||
|
||||
- the skill must already exist
|
||||
- `oldText` must be present exactly
|
||||
- only the first exact match is replaced
|
||||
|
||||
All writes are atomic and refresh the in-memory skills snapshot immediately, so
|
||||
the new or updated skill can become visible without a Gateway restart.
|
||||
|
||||
## Safety model
|
||||
|
||||
Skill Workshop has a safety scanner on generated `SKILL.md` content and support
|
||||
files.
|
||||
|
||||
Critical findings quarantine proposals:
|
||||
|
||||
| Rule id | Blocks content that... |
|
||||
| -------------------------------------- | --------------------------------------------------------------------- |
|
||||
| `prompt-injection-ignore-instructions` | tells the agent to ignore prior/higher instructions |
|
||||
| `prompt-injection-system` | references system prompts, developer messages, or hidden instructions |
|
||||
| `prompt-injection-tool` | encourages bypassing tool permission/approval |
|
||||
| `shell-pipe-to-shell` | includes `curl`/`wget` piped into `sh`, `bash`, or `zsh` |
|
||||
| `secret-exfiltration` | appears to send env/process env data over the network |
|
||||
|
||||
Warn findings are retained but do not block by themselves:
|
||||
|
||||
| Rule id | Warns on... |
|
||||
| -------------------- | -------------------------------- |
|
||||
| `destructive-delete` | broad `rm -rf` style commands |
|
||||
| `unsafe-permissions` | `chmod 777` style permission use |
|
||||
|
||||
Quarantined proposals:
|
||||
|
||||
- keep `scanFindings`
|
||||
- keep `quarantineReason`
|
||||
- appear in `list_quarantine`
|
||||
- cannot be applied through `apply`
|
||||
|
||||
To recover from a quarantined proposal, create a new safe proposal with the
|
||||
unsafe content removed. Do not edit the store JSON by hand.
|
||||
|
||||
## Prompt guidance
|
||||
|
||||
When enabled, Skill Workshop injects a short prompt section that tells the agent
|
||||
to use `skill_workshop` for durable procedural memory.
|
||||
|
||||
The guidance emphasizes:
|
||||
|
||||
- procedures, not facts/preferences
|
||||
- user corrections
|
||||
- non-obvious successful procedures
|
||||
- recurring pitfalls
|
||||
- stale/thin/wrong skill repair through append/replace
|
||||
- saving reusable procedure after long tool loops or hard fixes
|
||||
- short imperative skill text
|
||||
- no transcript dumps
|
||||
|
||||
The write mode text changes with `approvalPolicy`:
|
||||
|
||||
- pending mode: queue suggestions; use `apply` after explicit approval
|
||||
- auto mode: apply safe workspace-skill updates unless `apply: false` queues instead
|
||||
|
||||
## Costs and runtime behavior
|
||||
|
||||
Heuristic capture does not call a model.
|
||||
|
||||
LLM review uses an embedded run on the active/default agent model. It is
|
||||
threshold-based so it does not run on every turn by default.
|
||||
|
||||
The reviewer:
|
||||
|
||||
- uses the same configured provider/model context when available
|
||||
- falls back to runtime agent defaults
|
||||
- has `reviewTimeoutMs`
|
||||
- uses lightweight bootstrap context
|
||||
- has no tools
|
||||
- writes nothing directly
|
||||
- can only emit a proposal that goes through the normal scanner and
|
||||
approval/quarantine path
|
||||
|
||||
If the reviewer fails, times out, or returns invalid JSON, the plugin logs a
|
||||
warning/debug message and skips that review pass.
|
||||
|
||||
## Operating patterns
|
||||
|
||||
Use Skill Workshop when the user says:
|
||||
|
||||
- "next time, do X"
|
||||
- "from now on, prefer Y"
|
||||
- "make sure to verify Z"
|
||||
- "save this as a workflow"
|
||||
- "this took a while; remember the process"
|
||||
- "update the local skill for this"
|
||||
|
||||
Good skill text:
|
||||
|
||||
```markdown
|
||||
## Workflow
|
||||
|
||||
- Verify the GIF URL resolves to `image/gif`.
|
||||
- Confirm the file has multiple frames.
|
||||
- Record source URL, license, and attribution.
|
||||
- Store a local copy when the asset will ship with the product.
|
||||
- Verify the local asset renders in the target UI before final reply.
|
||||
```
|
||||
|
||||
Poor skill text:
|
||||
|
||||
```markdown
|
||||
The user asked about a GIF and I searched two websites. Then one was blocked by
|
||||
Cloudflare. The final answer said to check attribution.
|
||||
```
|
||||
|
||||
Reasons the poor version should not be saved:
|
||||
|
||||
- transcript-shaped
|
||||
- not imperative
|
||||
- includes noisy one-off details
|
||||
- does not tell the next agent what to do
|
||||
|
||||
## Debugging
|
||||
|
||||
Check whether the plugin is loaded:
|
||||
|
||||
```bash
|
||||
openclaw plugins list --enabled
|
||||
```
|
||||
|
||||
Check proposal counts from an agent/tool context:
|
||||
|
||||
```json
|
||||
{ "action": "status" }
|
||||
```
|
||||
|
||||
Inspect pending proposals:
|
||||
|
||||
```json
|
||||
{ "action": "list_pending" }
|
||||
```
|
||||
|
||||
Inspect quarantined proposals:
|
||||
|
||||
```json
|
||||
{ "action": "list_quarantine" }
|
||||
```
|
||||
|
||||
Common symptoms:
|
||||
|
||||
| Symptom | Likely cause | Check |
|
||||
| ------------------------------------- | ----------------------------------------------------------------------------------- | -------------------------------------------------------------------- |
|
||||
| Tool is unavailable | Plugin entry is not enabled | `plugins.entries.skill-workshop.enabled` and `openclaw plugins list` |
|
||||
| No automatic proposal appears | `autoCapture: false`, `reviewMode: "off"`, or thresholds not met | Config, proposal status, Gateway logs |
|
||||
| Heuristic did not capture | User wording did not match correction patterns | Use explicit `skill_workshop.suggest` or enable LLM reviewer |
|
||||
| Reviewer did not create a proposal | Reviewer returned `none`, invalid JSON, or timed out | Gateway logs, `reviewTimeoutMs`, thresholds |
|
||||
| Proposal is not applied | `approvalPolicy: "pending"` | `list_pending`, then `apply` |
|
||||
| Proposal disappeared from pending | Duplicate proposal reused, max pending pruning, or was applied/rejected/quarantined | `status`, `list_pending` with status filters, `list_quarantine` |
|
||||
| Skill file exists but model misses it | Skill snapshot not refreshed or skill gating excludes it | `openclaw skills` status and workspace skill eligibility |
|
||||
|
||||
Relevant logs:
|
||||
|
||||
- `skill-workshop: queued <skill>`
|
||||
- `skill-workshop: applied <skill>`
|
||||
- `skill-workshop: quarantined <skill>`
|
||||
- `skill-workshop: heuristic capture skipped: ...`
|
||||
- `skill-workshop: reviewer skipped: ...`
|
||||
- `skill-workshop: reviewer found no update`
|
||||
|
||||
## QA scenarios
|
||||
|
||||
Repo-backed QA scenarios:
|
||||
|
||||
- `qa/scenarios/plugins/skill-workshop-animated-gif-autocreate.md`
|
||||
- `qa/scenarios/plugins/skill-workshop-pending-approval.md`
|
||||
- `qa/scenarios/plugins/skill-workshop-reviewer-autonomous.md`
|
||||
|
||||
Run the deterministic coverage:
|
||||
|
||||
```bash
|
||||
pnpm openclaw qa suite \
|
||||
--scenario skill-workshop-animated-gif-autocreate \
|
||||
--scenario skill-workshop-pending-approval \
|
||||
--concurrency 1
|
||||
```
|
||||
|
||||
Run reviewer coverage:
|
||||
|
||||
```bash
|
||||
pnpm openclaw qa suite \
|
||||
--scenario skill-workshop-reviewer-autonomous \
|
||||
--concurrency 1
|
||||
```
|
||||
|
||||
The reviewer scenario is intentionally separate because it enables
|
||||
`reviewMode: "llm"` and exercises the embedded reviewer pass.
|
||||
|
||||
## When not to enable auto apply
|
||||
|
||||
Avoid `approvalPolicy: "auto"` when:
|
||||
|
||||
- the workspace contains sensitive procedures
|
||||
- the agent is working on untrusted input
|
||||
- skills are shared across a broad team
|
||||
- you are still tuning prompts or scanner rules
|
||||
- the model frequently handles hostile web/email content
|
||||
|
||||
Use pending mode first. Switch to auto mode only after reviewing the kind of
|
||||
skills the agent proposes in that workspace.
|
||||
|
||||
## Related docs
|
||||
|
||||
- [Skills](/tools/skills)
|
||||
- [Plugins](/tools/plugin)
|
||||
- [Testing](/reference/test)
|
||||
@@ -732,7 +732,7 @@ openclaw voicecall dtmf --call-id <id> --digits "ww123456#"
|
||||
openclaw voicecall end --call-id <id>
|
||||
openclaw voicecall status --call-id <id>
|
||||
openclaw voicecall tail
|
||||
openclaw voicecall latency # summarize turn latency from logs
|
||||
openclaw voicecall latency # summarize turn latency from SQLite call records
|
||||
openclaw voicecall expose --mode funnel
|
||||
```
|
||||
|
||||
@@ -741,9 +741,8 @@ to the Gateway-owned voice-call runtime so the CLI does not bind a second
|
||||
webhook server. If no Gateway is reachable, the commands fall back to a
|
||||
standalone CLI runtime.
|
||||
|
||||
`latency` reads `calls.jsonl` from the default voice-call storage path.
|
||||
Use `--file <path>` to point at a different log and `--last <n>` to limit
|
||||
analysis to the last N records (default 200). Output includes p50/p90/p99
|
||||
`latency` reads the SQLite-backed voice-call plugin state. Use `--last <n>` to
|
||||
limit analysis to the last N records (default 200). Output includes p50/p90/p99
|
||||
for turn latency and listen-wait times.
|
||||
|
||||
## Agent tool
|
||||
|
||||
Reference in New Issue
Block a user