mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-13 01:31:48 +08:00
Compare commits
217 Commits
codex/acti
...
fix/telegr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5e45e7b790 | ||
|
|
b3e26b1192 | ||
|
|
9b0f7e0e82 | ||
|
|
0950bdf727 | ||
|
|
06008b9b4a | ||
|
|
8d79b87dc7 | ||
|
|
95e397a266 | ||
|
|
7834140bf9 | ||
|
|
092c56ce46 | ||
|
|
a5b54e7c01 | ||
|
|
54f078dc86 | ||
|
|
6f8ad56b09 | ||
|
|
34c1f43df1 | ||
|
|
f329a01e69 | ||
|
|
93e509ccfe | ||
|
|
6681878339 | ||
|
|
37e667c4c5 | ||
|
|
492e98a88a | ||
|
|
45542fa726 | ||
|
|
945775007d | ||
|
|
d99e8ce619 | ||
|
|
be530f085d | ||
|
|
a04b9a27fb | ||
|
|
f38035a7b6 | ||
|
|
ed846a7157 | ||
|
|
363c2bc171 | ||
|
|
2e1ec9653c | ||
|
|
e52cf224df | ||
|
|
3574aedd68 | ||
|
|
490c9c80ef | ||
|
|
290bde2c14 | ||
|
|
279739d5c2 | ||
|
|
952862b9e2 | ||
|
|
73c475023f | ||
|
|
4260ac4cf6 | ||
|
|
2e7a0fc7fb | ||
|
|
680c0f77cb | ||
|
|
a385121475 | ||
|
|
7da23c36a1 | ||
|
|
6507bc0294 | ||
|
|
21d9bac5ec | ||
|
|
55cbcd829d | ||
|
|
8cbd60d203 | ||
|
|
2205153ee8 | ||
|
|
dc1e07ea41 | ||
|
|
c727ac48d8 | ||
|
|
2f5b5b7e35 | ||
|
|
3e7e6f2f60 | ||
|
|
95754cf57a | ||
|
|
4f5c137f88 | ||
|
|
3eb47e9e73 | ||
|
|
23fd3d32fb | ||
|
|
0a5aefefbd | ||
|
|
3b1c6d3266 | ||
|
|
f0f405cf47 | ||
|
|
31b0d97c33 | ||
|
|
9a65a5166f | ||
|
|
8a9a3984e4 | ||
|
|
1c5e4de3b0 | ||
|
|
890a0e4a99 | ||
|
|
2920bc7a70 | ||
|
|
b1a216c365 | ||
|
|
fd5be4bcc0 | ||
|
|
0486c736fb | ||
|
|
d398ed0660 | ||
|
|
f4c64168e7 | ||
|
|
abe7b2c49d | ||
|
|
e76542f9a0 | ||
|
|
17a2290f49 | ||
|
|
b0c5d2baf6 | ||
|
|
a53c13fc06 | ||
|
|
a34602c172 | ||
|
|
75fe554db7 | ||
|
|
6c0d25cea4 | ||
|
|
82c06e5604 | ||
|
|
993abc1fb9 | ||
|
|
825028289b | ||
|
|
fd68c28164 | ||
|
|
d7c3210cd6 | ||
|
|
b4034b32c3 | ||
|
|
5609a35f67 | ||
|
|
4ab6a7b324 | ||
|
|
d4e5f250a0 | ||
|
|
b73d8ef7d7 | ||
|
|
5eab61b45d | ||
|
|
e89dc72c35 | ||
|
|
357fcaea12 | ||
|
|
9ece252a65 | ||
|
|
8069b990a6 | ||
|
|
a4b9755999 | ||
|
|
2c5b534f65 | ||
|
|
9163e5bed7 | ||
|
|
d03fa0899f | ||
|
|
5982f2e5e4 | ||
|
|
9d31c5ad53 | ||
|
|
d9d9d357b4 | ||
|
|
dce3abaef7 | ||
|
|
5024ff7129 | ||
|
|
b2456e8037 | ||
|
|
4f8471617a | ||
|
|
5de5258897 | ||
|
|
99db33eb39 | ||
|
|
9eacd29138 | ||
|
|
f180474c2d | ||
|
|
5f6ea077af | ||
|
|
c4efdeddd5 | ||
|
|
81969c7a91 | ||
|
|
b8f12d99b2 | ||
|
|
5050017543 | ||
|
|
7fc3197ecb | ||
|
|
6807e6a89b | ||
|
|
87b31c8d58 | ||
|
|
da858c326b | ||
|
|
c33ad415df | ||
|
|
6211e3dcd6 | ||
|
|
c5392f3640 | ||
|
|
9a165e25ac | ||
|
|
3c9371ec60 | ||
|
|
dbaf4df493 | ||
|
|
0cb7168bc4 | ||
|
|
a8f8df7317 | ||
|
|
0e91c25c0b | ||
|
|
8f30a6c4ec | ||
|
|
a9e17db938 | ||
|
|
7d7648872b | ||
|
|
783a6fb5f3 | ||
|
|
bb15b7c53c | ||
|
|
e80ae14771 | ||
|
|
02d41b98c0 | ||
|
|
e5d0dbdc7c | ||
|
|
b52f106533 | ||
|
|
aec24f4599 | ||
|
|
e0b4f3b995 | ||
|
|
bf03babd2b | ||
|
|
08cee3316d | ||
|
|
3174c6919d | ||
|
|
a96a1aa670 | ||
|
|
63e00b811e | ||
|
|
3ff56020b1 | ||
|
|
4cfa4b95c3 | ||
|
|
ae1cc2d6df | ||
|
|
df91db906f | ||
|
|
ca8685d5f2 | ||
|
|
ce07a38f0c | ||
|
|
700efe6d16 | ||
|
|
381d229699 | ||
|
|
bdcf8b9796 | ||
|
|
27f122c4bf | ||
|
|
8359e5f584 | ||
|
|
29decd58dd | ||
|
|
c907dfa058 | ||
|
|
c1d08e3ddc | ||
|
|
cddfd7781e | ||
|
|
b998156083 | ||
|
|
51633fc13a | ||
|
|
4d3c72a521 | ||
|
|
259e9abbc5 | ||
|
|
93d71acbd8 | ||
|
|
7f508755a1 | ||
|
|
552b5d3859 | ||
|
|
f9dcd1e155 | ||
|
|
c01666f7ab | ||
|
|
cda0c66258 | ||
|
|
f6bf8c7202 | ||
|
|
6bd480ea1f | ||
|
|
1c1eb542b6 | ||
|
|
5e2736089d | ||
|
|
ef903d881e | ||
|
|
ee0425a705 | ||
|
|
b3ecabbbb7 | ||
|
|
669b352d36 | ||
|
|
c7ecc1ebf4 | ||
|
|
134588fc17 | ||
|
|
65ea8c60f3 | ||
|
|
e3b9c4a0a3 | ||
|
|
c7347a492e | ||
|
|
96fe85fb77 | ||
|
|
aaa88398bf | ||
|
|
7a28572caa | ||
|
|
1868f301ed | ||
|
|
5fa3b8d7a0 | ||
|
|
8fae182531 | ||
|
|
f738297bda | ||
|
|
d2fa1de7ba | ||
|
|
d51f527cca | ||
|
|
6f159a9a28 | ||
|
|
6affd09dbe | ||
|
|
d1019eaa9f | ||
|
|
5f5ec841fb | ||
|
|
a80453f2e1 | ||
|
|
ae5118a8b5 | ||
|
|
a51488c13c | ||
|
|
7e2fc57858 | ||
|
|
c0aed59fca | ||
|
|
11185f6397 | ||
|
|
d471bbc94d | ||
|
|
b2169c4295 | ||
|
|
fdf60c06b0 | ||
|
|
7999767a0f | ||
|
|
67035a6af8 | ||
|
|
7897fb9c84 | ||
|
|
88ac6f1194 | ||
|
|
b6970865b6 | ||
|
|
5a020cf9a1 | ||
|
|
e0ad3e79e6 | ||
|
|
fa82193c72 | ||
|
|
423a14e2be | ||
|
|
81a613f687 | ||
|
|
1a72ed7e13 | ||
|
|
a31cb15561 | ||
|
|
67dc6e82b9 | ||
|
|
775b78e186 | ||
|
|
2187b19d7e | ||
|
|
a44a26f0a0 | ||
|
|
af4a2faa1d | ||
|
|
5fb6aeaf86 | ||
|
|
b9e972e174 |
@@ -16,6 +16,7 @@ Use this skill for Parallels guest workflows and smoke interpretation. Do not lo
|
||||
- Pass `--json` for machine-readable summaries.
|
||||
- Per-phase logs land under `/tmp/openclaw-parallels-*`.
|
||||
- Do not run local and gateway agent turns in parallel on the same fresh workspace or session.
|
||||
- Do not run multiple smoke lanes against the same guest family at once. Tahoe lanes share the host HTTP port, and Windows/Linux lanes can collide on snapshot restore/start state if two jobs touch the same VM concurrently.
|
||||
- If `main` is moving under active multi-agent work, prefer a detached worktree pinned to one commit for long Parallels suites. The smoke scripts now verify the packed tgz commit instead of live `git rev-parse HEAD`, but a pinned worktree still avoids noisy rebuild/version drift during reruns.
|
||||
- For `openclaw update --channel dev` lanes, remember the guest clones GitHub `main`, not your local worktree. If a local fix exists but the rerun still fails inside the cloned dev checkout, do not treat that as disproof of the fix until the branch has been pushed.
|
||||
- For `prlctl exec`, pass the VM name before `--current-user` (`prlctl exec "$VM" --current-user ...`), not the other way around.
|
||||
@@ -33,6 +34,7 @@ Use this skill for Parallels guest workflows and smoke interpretation. Do not lo
|
||||
- The aggregate npm-update wrapper must resolve the Linux VM with the same Ubuntu fallback policy as `parallels-linux-smoke.sh` before both fresh and update lanes. Treat any Ubuntu guest with major version `>= 24` as acceptable when the exact default VM is missing, preferring the closest version match. On Peter's current host today, missing `Ubuntu 24.04.3 ARM64` should fall back to `Ubuntu 25.10`.
|
||||
- On macOS same-guest update checks, restart the gateway after the npm upgrade before `gateway status` / `agent`; launchd can otherwise report a loaded service while the old process has exited and the fresh process is not RPC-ready yet.
|
||||
- On Windows same-guest update checks, restart the gateway after the npm upgrade before `gateway status` / `agent`; in-place global npm updates can otherwise leave stale hashed `dist/*` module imports alive in the running service.
|
||||
- In those Windows same-guest update checks, do not treat one nonzero `openclaw gateway restart` as definitive failure. Current login-item restarts can report failure before the background service becomes observable again; follow with a longer RPC-ready wait and use `gateway start` only as a recovery step if readiness still never returns.
|
||||
- After that Windows restart, do not trust one `gateway status --deep --require-rpc` call after a fixed sleep. Retry the RPC-ready probe for roughly 30 seconds and log each attempt; current guests can keep port `18789` bound while the fresh RPC endpoint is still coming up.
|
||||
- For Windows same-guest update checks, prefer the done-file/log-drain PowerShell runner pattern over one long-lived `prlctl exec ... powershell -EncodedCommand ...` transport. The guest can finish successfully while the outer `prlctl exec` still hangs.
|
||||
- The Windows same-guest update helper should write stage markers to its log before long steps like tgz download and `npm install -g` so the outer progress monitor does not sit on `waiting for first log line` during healthy but quiet installs.
|
||||
@@ -57,6 +59,7 @@ Use this skill for Parallels guest workflows and smoke interpretation. Do not lo
|
||||
- On Peter's Tahoe VM, `fresh-latest-march-2026` can hang in `prlctl snapshot-switch`; if restore times out there, rerun with `--snapshot-hint 'macOS 26.3.1 latest'` before blaming auth or the harness.
|
||||
- `parallels-macos-smoke.sh` now retries `snapshot-switch` once after force-stopping a stuck running/suspended guest. If Tahoe still times out after that recovery path, then treat it as a real Parallels/host issue and rerun manually.
|
||||
- The macOS smoke should include a dashboard load phase after gateway health: resolve the tokenized URL with `openclaw dashboard --no-open`, verify the served HTML contains the Control UI title/root shell, then open Safari and require an established localhost TCP connection from Safari to the gateway port.
|
||||
- For Tahoe `fresh.gateway-status`, prefer non-TTY `prlctl exec --current-user ... openclaw gateway status ...` plus a few short retries. `prlctl enter` can spam TTY control bytes and hang the phase log even when the CLI itself is healthy.
|
||||
- If a Tahoe lane times out in `fresh.first-agent-turn` and the phase log stops right after `__OPENCLAW_RC__:0` from `models set`, suspect the `prlctl enter` / `expect` wrapper before blaming auth or the model lane. That pattern means the first guest command finished but the transport never released for the next `guest_current_user_cli` call.
|
||||
- If a packaged install regresses with `500` on `/`, `/healthz`, or `__openclaw/control-ui-config.json` after `fresh.install-main` or `upgrade.install-main`, suspect bundled plugin runtime deps resolving from the package root `node_modules` rather than `dist/extensions/*/node_modules`. Repro quickly with a real `npm pack`/global install lane before blaming dashboard auth or Safari.
|
||||
- `prlctl exec` is fine for deterministic repo commands, but use the guest Terminal or `prlctl enter` when installer parity or shell-sensitive behavior matters.
|
||||
@@ -88,7 +91,7 @@ Use this skill for Parallels guest workflows and smoke interpretation. Do not lo
|
||||
- Fresh Windows tgz install phases should also use the background PowerShell runner plus done-file/log-drain pattern; do not rely on one long-lived `prlctl exec ... powershell ... npm install -g` transport for package installs.
|
||||
- Windows release-to-dev helpers should log `where pnpm` before and after the update and require `where pnpm` to succeed post-update. That proves the updater installed or enabled `pnpm` itself instead of depending on a smoke-only bootstrap.
|
||||
- Fresh Windows ref-mode onboard should use the same background PowerShell runner plus done-file/log-drain pattern as the npm-update helper, including startup materialization checks, host-side timeouts on short poll `prlctl exec` calls, and retry-on-poll-failure behavior for transient transport flakes.
|
||||
- Fresh Windows daemon-health reachability should use a hello-only gateway probe and a longer per-probe timeout than the default local attach path; full health RPCs are too eager during initial startup on current main.
|
||||
- Fresh Windows daemon-health reachability should use `openclaw gateway probe --json` with a longer timeout and treat `ok: true` as success; full `gateway status --require-rpc` checks are too eager during initial startup on current main.
|
||||
- Fresh Windows ref-mode agent verification should set `OPENAI_API_KEY` in the PowerShell environment before invoking `openclaw.cmd agent`, for the same pairing-required fallback reason as macOS.
|
||||
- The standalone Windows upgrade smoke lane should stop the managed gateway after `upgrade.install-main` and before `upgrade.onboard-ref`. Restarting before onboard can leave the old process alive on the pre-onboard token while onboard rewrites `~/.openclaw/openclaw.json`, which then fails `gateway-health` with `unauthorized: gateway token mismatch`.
|
||||
- If standalone Windows upgrade fails with a gateway token mismatch but `pnpm test:parallels:npm-update` passes, trust the mismatch as a standalone ref-onboard ordering bug first; the npm-update helper does not re-run ref-mode onboard on the same guest.
|
||||
|
||||
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@@ -450,6 +450,7 @@ jobs:
|
||||
|
||||
- name: Run ${{ matrix.task }} (${{ matrix.runtime }})
|
||||
env:
|
||||
OPENCLAW_TEST_PROJECTS_PARALLEL: 3
|
||||
TASK: ${{ matrix.task }}
|
||||
shell: bash
|
||||
run: |
|
||||
@@ -548,6 +549,9 @@ jobs:
|
||||
TASK: ${{ matrix.task }}
|
||||
run: |
|
||||
echo "OPENCLAW_VITEST_MAX_WORKERS=2" >> "$GITHUB_ENV"
|
||||
if [ "$TASK" = "test" ]; then
|
||||
echo "OPENCLAW_TEST_PROJECTS_PARALLEL=6" >> "$GITHUB_ENV"
|
||||
fi
|
||||
if [ "$TASK" = "channels" ]; then
|
||||
echo "OPENCLAW_VITEST_MAX_WORKERS=1" >> "$GITHUB_ENV"
|
||||
fi
|
||||
|
||||
165
CHANGELOG.md
165
CHANGELOG.md
@@ -6,97 +6,127 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Changes
|
||||
|
||||
- Memory/Active Memory: add a new optional Active Memory plugin that gives OpenClaw a dedicated memory subagent right before the main reply, so ongoing chats can automatically pull in relevant preferences, context, and past details without making users remember to manually say "remember this" or "search memory" first. Includes configurable message/recent/full context modes, live `/verbose` inspection, and opt-in transcript persistence for debugging.
|
||||
- CLI/infer: add a first-class `openclaw infer ...` hub for provider-backed inference workflows across model, media, web, and embedding tasks. Thanks @Takhoffman.
|
||||
- Plugins/webhooks: add a bundled webhook ingress plugin so external automation can create and drive bound TaskFlows through per-route shared-secret endpoints. (#61892) Thanks @mbelinky.
|
||||
- Tools/media generation: preserve intent across auth-backed image, music, and video provider fallback, remap size, aspect ratio, resolution, and duration hints to the closest supported option, and surface explicit provider capabilities plus mode-aware video-to-video support.
|
||||
- Memory/wiki: restore the bundled `memory-wiki` stack with plugin, CLI, sync/query/apply tooling, and memory-host integration for wiki-backed memory workflows.
|
||||
- Providers/Arcee AI: add a bundled Arcee AI provider plugin with Trinity catalog entries, OpenRouter support, and updated onboarding/auth guidance. (#62068) Thanks @arthurbr11.
|
||||
- Providers/Google: add Gemma 4 model support and keep Google fallback resolution on the requested provider path so native Google Gemma routes work again. (#61507) Thanks @eyjohn.
|
||||
- Providers/Google: preserve explicit thinking-off semantics for Gemma 4 while still enabling Gemma reasoning support in the Google compatibility wrappers. (#62127) Thanks @romgenie, co-authored with BunsDev.
|
||||
- Providers/Anthropic: restore Claude CLI as the preferred local Anthropic path in onboarding, model-auth guidance, doctor flows, and Docker Claude CLI live lanes again.
|
||||
- ACP/ACPX plugin: bump the bundled `acpx` pin to `0.5.1` so plugin-local installs and strict version checks pick up the latest published runtime release. (#62148) Thanks @onutc.
|
||||
- Tools/media generation: auto-fallback across auth-backed image, music, and video providers by default, and remap fallback size, aspect ratio, resolution, and duration hints to the closest supported option instead of dropping intent on provider switches.
|
||||
- Tools/media generation: report applied fallback geometry and duration settings consistently in tool results, add a shared normalization contract for image/music/video runtimes, and simplify the bundled image-generation-core runtime test to only verify the plugin-sdk re-export seam.
|
||||
- Gateway/sessions: add persisted compaction checkpoints plus Sessions UI branch/restore actions so operators can inspect and recover pre-compaction session state. (#62146) Thanks @scoootscooob.
|
||||
- Providers/Ollama: detect vision capability from the `/api/show` response and set image input on models that support it so Ollama vision models accept image attachments. (#62193) Thanks @BruceMacD.
|
||||
- Memory/dreaming: ingest redacted session transcripts into the dreaming corpus with per-day session-corpus notes, cursor checkpointing, and promotion/doctor support. (#62227) Thanks @vignesh07.
|
||||
- Plugins/memory: add a public memory-artifact export seam to the unified memory capability so companion plugins like `memory-wiki` can bridge the active memory plugin without reaching into `memory-core` internals. Thanks @vincentkoc.
|
||||
- Memory/wiki: add structured claim/evidence fields plus compiled agent digest artifacts so `memory-wiki` behaves more like a persistent knowledge layer and less like markdown-only page storage. Thanks @vincentkoc.
|
||||
- Memory/wiki: add claim-health linting, contradiction clustering, staleness-aware dashboards, and freshness-weighted wiki search so `memory-wiki` can act more like a maintained belief layer than a passive markdown dump. Thanks @vincentkoc.
|
||||
- Memory/wiki: use compiled digest artifacts as the first-pass wiki index for search/get flows, and resolve claim ids back to owning pages so agents can retrieve knowledge by belief identity instead of only by file path. Thanks @vincentkoc.
|
||||
- Memory/wiki: add an opt-in `context.includeCompiledDigestPrompt` flag so memory prompt supplements can append a compact compiled wiki snapshot for legacy prompt assembly and context engines that explicitly consume memory prompt sections. Thanks @vincentkoc.
|
||||
- Plugin SDK/context engines: pass `availableTools` and `citationsMode` into `assemble()`, and expose `buildMemorySystemPromptAddition(...)` so non-legacy context engines can adopt the active memory prompt path without reimplementing it. Thanks @vincentkoc.
|
||||
- Providers/inferrs: add string-content compatibility for stricter OpenAI-compatible chat backends, document `inferrs` setup with a full config example, and add troubleshooting guidance for local backends that pass direct probes but fail on full agent-runtime prompts.
|
||||
- Agents/context engine: expose prompt-cache runtime context to context engines and keep current-turn prompt-cache usage aligned with the active attempt instead of stale prior-turn assistant state. (#62179) Thanks @jalehman.
|
||||
- Tools/media: document per-provider music and video generation capabilities, and add shared live video-to-video sweep coverage for providers that support local reference clips.
|
||||
- Compaction: add pluggable compaction provider registry so plugins can replace the built-in summarization pipeline. Configure via `agents.defaults.compaction.provider`; falls back to LLM summarization on provider failure. (#56224) Thanks @DhruvBhatia0.
|
||||
- Discord/events: allow `event-create` to accept a cover image URL or local file path, load and validate PNG/JPG/GIF event cover media, and pass the encoded image payload through Discord admin action/runtime paths. (#60883) Thanks @bittoby.
|
||||
- iOS: pin release versioning to an explicit CalVer in `apps/ios/version.json`, keep TestFlight iteration on the same short version until maintainers intentionally promote the next gateway version, and add the documented `pnpm ios:version:pin -- --from-gateway` workflow for release trains. (#63001) Thanks @ngutman.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Slack/media: preserve bearer auth across same-origin `files.slack.com` redirects while still stripping it on cross-origin Slack CDN hops, so `url_private_download` image attachments load again. (#62960) Thanks @vincentkoc.
|
||||
- Control UI: guard stale session-history reloads during fast session switches so the selected session and rendered transcript stay in sync. (#62975) Thanks @scoootscooob.
|
||||
|
||||
## 2026.4.8
|
||||
|
||||
### Fixes
|
||||
|
||||
- Telegram/setup: load setup and secret contracts through packaged top-level sidecars so installed npm builds no longer try to import missing `dist/extensions/telegram/src/*` files during gateway startup.
|
||||
- Bundled channels/setup: load shared secret contracts through packaged top-level sidecars across BlueBubbles, Feishu, Google Chat, IRC, Matrix, Mattermost, Microsoft Teams, Nextcloud Talk, Slack, and Zalo so installed npm builds no longer rely on missing `dist/extensions/*/src/*` files during gateway startup.
|
||||
- Bundled plugins: align packaged plugin compatibility metadata with the release version so bundled channels and providers load on OpenClaw 2026.4.8.
|
||||
- Agents/progress: keep `update_plan` available for OpenAI-family runs while returning compact success payloads and allowing `tools.experimental.planTool=false` to opt out.
|
||||
- Agents/exec: keep `/exec` current-default reporting aligned with real runtime behavior so `host=auto` sessions surface the correct host-aware fallback policy (`full/off` on gateway or node, `deny/off` on sandbox) instead of stale stricter defaults.
|
||||
- Slack: honor ambient HTTP(S) proxy settings for Socket Mode WebSocket connections, including NO_PROXY exclusions, so proxy-only deployments can connect without a monkey patch. (#62878) Thanks @mjamiv.
|
||||
- Slack/actions: pass the already resolved read token into `downloadFile` so SecretRef-backed bot tokens no longer fail after a raw config re-read. (#62097) Thanks @martingarramon.
|
||||
- Network/fetch guard: skip target DNS pinning when trusted env-proxy mode is active so proxy-only sandboxes can let the trusted proxy resolve outbound hosts. (#59007) Thanks @cluster2600.
|
||||
|
||||
## 2026.4.7
|
||||
|
||||
### Changes
|
||||
|
||||
- CLI/infer: add a first-class `openclaw infer ...` hub for provider-backed inference workflows across model, media, web, and embedding tasks. Thanks @Takhoffman.
|
||||
- Tools/media generation: auto-fallback across auth-backed image, music, and video providers by default, preserve intent during provider switches, remap size/aspect/resolution/duration hints to the closest supported option, and surface provider capabilities plus mode-aware video-to-video support.
|
||||
- Memory/wiki: restore the bundled `memory-wiki` stack with plugin, CLI, sync/query/apply tooling, memory-host integration, structured claim/evidence fields, compiled digest retrieval, claim-health linting, contradiction clustering, staleness dashboards, and freshness-weighted search. Thanks @vincentkoc.
|
||||
- Plugins/webhooks: add a bundled webhook ingress plugin so external automation can create and drive bound TaskFlows through per-route shared-secret endpoints. (#61892) Thanks @mbelinky.
|
||||
- Gateway/sessions: add persisted compaction checkpoints plus Sessions UI branch/restore actions so operators can inspect and recover pre-compaction session state. (#62146) Thanks @scoootscooob.
|
||||
- Compaction: add pluggable compaction provider registry so plugins can replace the built-in summarization pipeline. Configure via `agents.defaults.compaction.provider`; falls back to LLM summarization on provider failure. (#56224) Thanks @DhruvBhatia0.
|
||||
- Agents/system prompt: add `agents.defaults.systemPromptOverride` for controlled prompt experiments plus heartbeat prompt-section controls so heartbeat runtime behavior can stay enabled without injecting heartbeat instructions every turn.
|
||||
- Providers/Google: add Gemma 4 model support and keep Google fallback resolution on the requested provider path so native Google Gemma routes work again. (#61507) Thanks @eyjohn.
|
||||
- Providers/Google: preserve explicit thinking-off semantics for Gemma 4 while still enabling Gemma reasoning support in compatibility wrappers. (#62127) Thanks @romgenie.
|
||||
- Providers/Arcee AI: add a bundled Arcee AI provider plugin with Trinity catalog entries, OpenRouter support, and updated onboarding/auth guidance. (#62068) Thanks @arthurbr11.
|
||||
- Providers/Anthropic: restore Claude CLI as the preferred local Anthropic path in onboarding, model-auth guidance, doctor flows, and Docker Claude CLI live lanes again.
|
||||
- Providers/Ollama: detect vision capability from the `/api/show` response and set image input on models that support it so Ollama vision models accept image attachments. (#62193) Thanks @BruceMacD.
|
||||
- Memory/dreaming: ingest redacted session transcripts into the dreaming corpus with per-day session-corpus notes, cursor checkpointing, and promotion/doctor support. (#62227) Thanks @vignesh07.
|
||||
- Providers/inferrs: add string-content compatibility for stricter OpenAI-compatible chat backends, document `inferrs` setup with a full config example, and add troubleshooting guidance for local backends that pass direct probes but fail on full agent-runtime prompts.
|
||||
- Agents/context engine: expose prompt-cache runtime context to context engines and keep current-turn prompt-cache usage aligned with the active attempt instead of stale prior-turn assistant state. (#62179) Thanks @jalehman.
|
||||
- Plugin SDK/context engines: pass `availableTools` and `citationsMode` into `assemble()`, and expose memory-artifact and memory-prompt seams so companion plugins and non-legacy context engines can consume active memory state without reaching into internals. Thanks @vincentkoc.
|
||||
- ACP/ACPX plugin: bump the bundled `acpx` pin to `0.5.1` so plugin-local installs and strict version checks pick up the latest published runtime release. (#62148) Thanks @onutc.
|
||||
- Discord/events: allow `event-create` to accept a cover image URL or local file path, load and validate PNG/JPG/GIF event cover media, and pass the encoded image payload through Discord admin action/runtime paths. (#60883) Thanks @bittoby.
|
||||
- Plugins/provider-auth: expose runtime-ready provider auth through `openclaw/plugin-sdk/provider-auth-runtime` so native plugins and context engines can resolve request-ready credentials after provider-owned runtime exchanges like GitHub Copilot device-token-to-bearer flows. (#62753) Thanks @jalehman.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Plugins/media: when `plugins.allow` is set, capability fallback now merges bundled capability plugin ids into the allowlist (not only `plugins.entries`), so media understanding providers such as OpenAI-compatible STT load for voice transcription without requiring `openai` in `plugins.allow`. (#62205) Thanks @neeravmakwana.
|
||||
- CLI/infer: keep provider-backed infer behavior aligned with actual runtime execution by fixing explicit TTS override handling, profile-aware gateway TTS prefs resolution, per-request transcription `prompt`/`language` overrides, image output MIME/extension mismatches, configured web-search fallback behavior, and agent-vs-CLI web-search execution drift.
|
||||
- Auth/OpenAI Codex OAuth: reload fresh on-disk credentials inside the locked refresh path and retry once after `refresh_token_reused` rotates only the stored refresh token, so relogin/restart recovery stops getting stuck on stale cached auth state. Thanks @owen-ever.
|
||||
- Agents/history and replies: buffer phaseless OpenAI WS text until a real assistant phase arrives, keep replay and SSE history sequence tracking aligned, hide commentary and leaked tool XML from user-visible history, and keep history-based follow-up replies on `final_answer` text only. (#61729, #61747, #61829, #61855, #61954) Thanks @100yenadmin, @afurm, and @openperf.
|
||||
- Plugins/channels: keep bundled channel artifact and secret-contract loading stable under lazy loading, preserve plugin-schema defaults during install, and fix Windows `file://` plus native-Jiti plugin loader paths so onboarding, doctor, `openclaw secret`, and bundled plugin installs work again. (#61832, #61836, #61853, #61856) Thanks @Zeesejo and @SuperMarioYL.
|
||||
- Plugins/ClawHub: verify downloaded plugin archives against version metadata SHA-256, fail closed when archive integrity metadata is missing or malformed, and tighten fallback ZIP verification so plugin installs cannot proceed on mismatched or incomplete ClawHub package metadata. (#60517) Thanks @mappel-nv.
|
||||
- Auto-reply/media: allow managed generated-media `MEDIA:` paths from normal reply text again while still blocking arbitrary host-local media and document paths, so generated media keep delivering without reopening host-path injection holes.
|
||||
- Runtime event trust: mark background `notifyOnExit` summaries, ACP parent-stream relays, and wake-hook payloads as untrusted system events so lower-trust runtime output no longer re-enters later turns as trusted `System:` text. (#62003)
|
||||
- Providers/Anthropic: preserve thinking blocks for Claude Opus 4.5+, Sonnet 4.5+, and newer Claude 4-family models so prompt-cache prefixes keep matching, and skip `service_tier` injection on OAuth-authenticated stream wrapper requests so Claude OAuth streaming stops failing with HTTP 401. (#60356, #61793)
|
||||
- Plugins/media: when `plugins.allow` is set, capability fallback now merges bundled capability plugin ids into the allowlist (not only `plugins.entries`), so media understanding providers such as OpenAI-compatible STT load for voice transcription without requiring `openai` in `plugins.allow`. (#62205) Thanks @neeravmakwana.
|
||||
- Agents/history and replies: buffer phaseless OpenAI WS text until a real assistant phase arrives, keep replay and SSE history sequence tracking aligned, hide commentary and leaked tool XML from user-visible history, and keep history-based follow-up replies on `final_answer` text only. (#61729, #61747, #61829, #61855, #61954) Thanks @100yenadmin and contributors.
|
||||
- Control UI: show `/tts` audio replies in webchat, detect mistaken `?token=` auth links with the correct `#token=` hint, and keep Copy, Canvas, and mobile exec-approval UI from covering chat content on narrow screens. (#54842, #61514, #61598) Thanks @neeravmakwana.
|
||||
- TUI: route `/status` through the shared session-status command, keep commentary hidden in history, strip raw envelope metadata from async command notices, preserve fallback streaming before per-attempt failures finalize, and restore Kitty keyboard state on exit or fatal crashes. (#49130, #59985, #60043, #61463) Thanks @biefan, @MoerAI, @jwchmodx, and @100yenadmin.
|
||||
- Sessions/model selection: resolve the explicitly selected session model separately from runtime fallback resolution so session status and live model switching stay aligned with the chosen model.
|
||||
- Discord/ACP bindings: canonicalize DM conversation identity across inbound messages, component interactions, native commands, and current-conversation binding resolution so `--bind here` in Discord DMs keeps routing follow-up replies to the bound agent instead of falling back to the default agent.
|
||||
- iOS/gateway: replace string-matched connection error UI with structured gateway connection problems, preserve actionable pairing/auth failures over later generic disconnect noise, and surface reusable problem banners and details across onboarding, settings, and root status surfaces. (#62650) Thanks @ngutman.
|
||||
- TUI: route `/status` through the shared session-status command, keep commentary hidden in history, strip raw envelope metadata from async command notices, preserve fallback streaming before per-attempt failures finalize, and restore Kitty keyboard state on exit or fatal crashes. (#49130, #59985, #60043, #61463) Thanks @biefan and contributors.
|
||||
- iOS/Watch exec approvals: keep Apple Watch review and approval recovery working while the iPhone is locked or backgrounded, including reconnect recovery, pending approval persistence, notification cleanup, and APNs-backed watch refresh recovery. (#61757) Thanks @ngutman.
|
||||
- Agents/context overflow: combine oversized and aggregate tool-result recovery in one pass and restore a total-context overflow backstop so recoverable sessions retry instead of failing early. (#61651) Thanks @Takhoffman.
|
||||
- Auth/OpenAI Codex OAuth: reload fresh on-disk credentials inside the locked refresh path and retry once after `refresh_token_reused` rotates only the stored refresh token, so relogin/restart recovery stops getting stuck on stale cached auth state. Thanks @owen-ever.
|
||||
- Auth/OpenAI Codex OAuth: keep native `/model ...@profile` selections on the target session and honor explicit user-locked auth profiles even when per-agent auth order excludes them. (#62744) Thanks @jalehman.
|
||||
- Providers/Anthropic: preserve thinking blocks for Claude Opus 4.5+, Sonnet 4.5+, and newer Claude 4-family models so prompt-cache prefixes keep matching, and skip `service_tier` injection on OAuth-authenticated stream wrapper requests so Claude OAuth streaming stops failing with HTTP 401. (#60356, #61793)
|
||||
- Agents/Claude CLI: surface nested API error messages from structured CLI output so billing/auth/provider failures show the real provider error instead of an opaque CLI failure.
|
||||
- Agents/exec: preserve explicit `host=node` routing under elevated defaults when `tools.exec.host=auto`, fail loud on invalid elevated cross-host overrides, and keep `strictInlineEval` commands blocked after approval timeouts instead of falling through to automatic execution. (#61739) Thanks @obviyus.
|
||||
- Nodes/exec approvals: keep `host=node` POSIX transport shell wrappers (`/bin/sh -lc ...`) aligned with inner-command allowlist analysis so allowlisted scripts stop prompting unnecessarily, while Windows `cmd.exe` wrapper runs stay approval-gated. (#62401) Thanks @ngutman.
|
||||
- Nodes/exec approvals: keep Windows `cmd.exe /c` wrapper runs approval-gated even when `env` carriers, including env-assignment carriers, wrap the shell invocation. (#62439) Thanks @ngutman.
|
||||
- Agents/context overflow: combine oversized and aggregate tool-result recovery in one pass and restore a total-context overflow backstop so recoverable sessions retry instead of failing early. (#61651) Thanks @Takhoffman.
|
||||
- Agents/exec: preserve explicit `host=node` routing under elevated defaults when `tools.exec.host=auto`, fail loud on invalid elevated cross-host overrides, and keep `strictInlineEval` commands blocked after approval timeouts instead of falling through to automatic execution. (#61739) Thanks @obviyus.
|
||||
- Host exec/env sanitization: block dangerous `JAVA_OPTS`, `RUSTFLAGS`, and `CARGO_HOME` inputs at the host-exec boundary so attacker-controlled env overrides can no longer inject JVM agents, compiler flags, or Cargo state pivots into host-run processes. (#62291) Thanks @pgondhi987.
|
||||
- Gateway tool/exec config: block model-facing `gateway config.apply` and `config.patch` writes from changing exec approval paths such as `safeBins`, `safeBinProfiles`, `safeBinTrustedDirs`, and `strictInlineEval`, while still allowing unchanged structured values through. (#62001) Thanks @eleqtrizit.
|
||||
- Host exec/env sanitization: block dangerous Java, Rust, Cargo, Git, Kubernetes, cloud credential, config-path, and Helm env overrides so host-run tools cannot be redirected to attacker-chosen code, config, credentials, or repository state. (#59119, #62002, #62291) Thanks @eleqtrizit and contributors.
|
||||
- Commands/allowlist: require owner authorization for `/allowlist add` and `/allowlist remove` before channel resolution, so non-owner but command-authorized senders can no longer persistently rewrite allowlist policy state. (#62383) Thanks @pgondhi987.
|
||||
- Feishu/docx uploads: honor `tools.fs.workspaceOnly` for local `upload_file` and `upload_image` paths by forwarding workspace-constrained `localRoots` into the media loader, so docx uploads can no longer read host-local files outside the workspace when workspace-only mode is active. (#62369) Thanks @pgondhi987.
|
||||
- Network/fetch guard: drop request bodies and body-describing headers on cross-origin `307` and `308` redirects by default, so attacker-controlled redirect hops cannot receive secret-bearing POST payloads from SSRF-guarded fetch flows unless a caller explicitly opts in. (#62357) Thanks @pgondhi987.
|
||||
- Providers/Ollama: honor the selected provider's `baseUrl` during streaming so multi-Ollama setups stop routing every stream to the first configured Ollama endpoint. (#61678)
|
||||
- Browser/remote CDP: retry the DevTools websocket once after remote browser restarts so healthy remote browser profiles do not fail availability checks during CDP warm-up. (#57397) Thanks @ThanhNguyxn07.
|
||||
- Browser/SSRF: treat main-frame `document` redirect hops as navigations even when Playwright does not flag them as `isNavigationRequest()`, so strict private-network blocking still stops forbidden redirect pivots before the browser reaches the internal target. (#62355) Thanks @pgondhi987.
|
||||
- Browser/node invoke: block persistent browser profile create, reset, and delete mutations through `browser.proxy` on both gateway-forwarded `node.invoke` and the node-host proxy path, even when no profile allowlist is configured. (#60489)
|
||||
- Gateway/node pairing: require a fresh pairing request when a previously paired node reconnects with additional declared commands, and keep the live session pinned to the earlier approved command set until the upgrade is approved. (#62658) Thanks @eleqtrizit.
|
||||
- Gateway/auth: invalidate existing shared-token and password WebSocket sessions when the configured secret rotates, so stale authenticated sockets cannot stay attached after token or password changes. (#62350) Thanks @pgondhi987.
|
||||
- Gateway/status and containers: auto-bind to `0.0.0.0` inside Docker and Podman environments, and probe local TLS gateways over `wss://` with self-signed fingerprint forwarding so container startup and loopback TLS status checks work again. (#61818, #61935) Thanks @openperf and @ThanhNguyxn07.
|
||||
- macOS/gateway version: strip trailing commit metadata from CLI version output before semver parsing so the Mac app recognizes installed gateway versions like `OpenClaw 2026.4.2 (d74a122)` again. (#61111) Thanks @oliviareid-svg.
|
||||
- Discord: recover forwarded referenced message text and attachments when snapshots are missing, use `ws://` again for gateway monitor sockets, stop forcing a hardcoded temperature for Codex-backed auto-thread titles, and harden voice receive recovery so rapid speaker restarts keep their next utterance. (#41536, #61670) Thanks @artwalker and @wit-oc.
|
||||
- Slack/threading: keep legacy thread stickiness for real replies when older callers omit `isThreadReply`, while still honoring `replyToMode` for Slack's auto-created top-level `thread_ts`. (#61835) Thanks @kaonash.
|
||||
- Plugins/loaders: centralize bundled `dist/**` Jiti native-load policy and keep channel, public-surface, facade, and config-metadata loader seams off native Jiti on Windows so onboarding and configure flows stop tripping `ERR_UNSUPPORTED_ESM_URL_SCHEME`. (#62286) Thanks @chen-zhang-cs-code.
|
||||
- Providers/xAI: recognize `api.grok.x.ai` as an xAI-native endpoint again and keep legacy `x_search` auth resolution working so older xAI web-search configs continue to load. (#61377) Thanks @jjjojoj.
|
||||
- Memory/vector recall: surface explicit warnings when `sqlite-vec` is unavailable or vector writes are degraded, and strip managed Light Sleep and REM blocks before daily-note ingestion so memory indexing and dreaming stop reporting false-success or re-ingesting staged output. (#61720) Thanks @MonkeyLeeT.
|
||||
- Matrix/formatting: preserve multi-paragraph and loose-list rendering in Element so numbered and bulleted Markdown keeps their content attached to the correct list item. (#60997) Thanks @gucasbrg.
|
||||
- MS Teams/security: validate file-consent upload URLs against HTTPS, Microsoft/SharePoint host allowlists, and private-IP DNS checks before uploading attachments, blocking SSRF-style consent-upload abuse. (#23596)
|
||||
- QQ Bot/media: route gateway-side attachment and fallback downloads through guarded QQ/Tencent HTTPS fetches so QQ media handling no longer follows arbitrary remote hosts.
|
||||
- Providers/Ollama: stop warning that Ollama could not be reached when discovery only sees empty default local stubs, while still keeping real explicit Ollama overrides loud when the endpoint is unreachable.
|
||||
- Tools/web_fetch and web_search: fix `TypeError: fetch failed` caused by undici 8.0 enabling HTTP/2 by default; pinned SSRF-guard dispatchers now explicitly set `allowH2: false` to restore HTTP/1.1 behavior and keep the custom DNS-pinning lookup compatible. (#61738, #61777) Thanks @zozo123.
|
||||
- Tools/web search/Exa: show Exa Search in onboarding and configure provider pickers again by marking the bundled Exa provider as setup-visible. Thanks @vincentkoc.
|
||||
- Docs/i18n: relocalize final localized-page links after translation and remove the zh-CN homepage redirect override so localized Mintlify pages resolve to the correct language roots again. (#61796) Thanks @hxy91819.
|
||||
- Plugins/provider hooks: stop recursive provider snapshot loads from overflowing the stack during plugin initialization, while still preserving cached nested provider-hook results. (#61922, #61938, #61946, #61951)
|
||||
- Exec/runtime events: mark background `notifyOnExit` summaries and ACP parent-stream relays as untrusted system events so lower-trust runtime output no longer re-enters later turns as trusted `System:` text.
|
||||
- Hooks/wake: queue direct and mapped wake-hook payloads as untrusted system events so external wake content no longer enters the main session as trusted input. (#62003)
|
||||
- Browser/node invoke: block persistent browser profile create, reset, and delete mutations through `browser.proxy` on both gateway-forwarded `node.invoke` and the node-host proxy path, even when no profile allowlist is configured. (#60489)
|
||||
- Media/base64 decode guards: enforce byte limits before decoding missed base64-backed Teams, Signal, QQ Bot, and image-tool payloads so oversized inbound media and data URLs no longer bypass pre-decode size checks. (#62007) Thanks @eleqtrizit.
|
||||
- Runtime event trust: mark background `notifyOnExit` summaries, ACP parent-stream relays, and wake-hook payloads as untrusted system events so lower-trust runtime output no longer re-enters later turns as trusted `System:` text. (#62003)
|
||||
- Auto-reply/media: allow managed generated-media `MEDIA:` paths from normal reply text again while still blocking arbitrary host-local media and document paths, so generated media keep delivering without reopening host-path injection holes.
|
||||
- Gateway/status and containers: auto-bind to `0.0.0.0` inside Docker and Podman environments, and probe local TLS gateways over `wss://` with self-signed fingerprint forwarding so container startup and loopback TLS status checks work again. (#61818, #61935) Thanks @openperf and contributors.
|
||||
- Gateway/OpenAI-compatible HTTP: abort in-flight `/v1/chat/completions` and `/v1/responses` turns when clients disconnect so abandoned HTTP requests stop wasting agent runtime. (#54388) Thanks @Lellansin.
|
||||
- macOS/gateway version: strip trailing commit metadata from CLI version output before semver parsing so the Mac app recognizes installed gateway versions like `OpenClaw 2026.4.2 (d74a122)` again. (#61111) Thanks @oliviareid-svg.
|
||||
- Sessions/model selection: resolve the explicitly selected session model separately from runtime fallback resolution so session status and live model switching stay aligned with the chosen model.
|
||||
- Discord/ACP bindings: canonicalize DM conversation identity across inbound messages, component interactions, native commands, and current-conversation binding resolution so `--bind here` in Discord DMs keeps routing follow-up replies to the bound agent instead of falling back to the default agent.
|
||||
- Discord: recover forwarded referenced message text and attachments when snapshots are missing, use `ws://` again for gateway monitor sockets, stop forcing a hardcoded temperature for Codex-backed auto-thread titles, and harden voice receive recovery so rapid speaker restarts keep their next utterance. (#41536, #61670) Thanks @artwalker and contributors.
|
||||
- Slack/thread mentions: add `channels.slack.thread.requireExplicitMention` so Slack channels that already require mentions can also require explicit `@bot` mentions inside bot-participated threads. (#58276) Thanks @praktika-engineer.
|
||||
- UI/light mode: target both root and nested WebKit scrollbar thumbs in the light theme so page-level and container scrollbars stay visible on light backgrounds. (#61753) Thanks @chziyue.
|
||||
- Matrix/onboarding: add an invite auto-join setup step with explicit off warnings and strict stable-target validation so new Matrix accounts stop silently ignoring invited rooms and fresh DM-style invites unless operators opt in. (#62168) Thanks @gumadeiras.
|
||||
- Telegram/doctor: keep top-level access-control fallback in place during multi-account normalization while still promoting legacy default auth into `accounts.default`, so existing named bots keep inherited allowlists without dropping the legacy default bot. (#62263) Thanks @obviyus.
|
||||
- Agents/subagents: honor `sessions_spawn(lightContext: true)` for spawned subagent runs by preserving lightweight bootstrap context through the gateway and embedded runner instead of silently falling back to full workspace bootstrap injection. (#62264) Thanks @theSamPadilla.
|
||||
- Slack/threading: keep legacy thread stickiness for real replies when older callers omit `isThreadReply`, while still honoring `replyToMode` for Slack's auto-created top-level `thread_ts`. (#61835) Thanks @kaonash.
|
||||
- Slack/media: keep attachment downloads on the SSRF-guarded dispatcher path so Slack media fetching works on Node 22 without dropping pinned transport enforcement. (#62239) Thanks @openperf.
|
||||
- Matrix/onboarding: add an invite auto-join setup step with explicit off warnings and strict stable-target validation so new Matrix accounts stop silently ignoring invited rooms and fresh DM-style invites unless operators opt in. (#62168) Thanks @gumadeiras.
|
||||
- Matrix/formatting: preserve multi-paragraph and loose-list rendering in Element so numbered and bulleted Markdown keeps their content attached to the correct list item. (#60997) Thanks @gucasbrg.
|
||||
- Telegram/doctor: keep top-level access-control fallback in place during multi-account normalization while still promoting legacy default auth into `accounts.default`, so existing named bots keep inherited allowlists without dropping the legacy default bot. (#62263) Thanks @obviyus.
|
||||
- Plugins/loaders: centralize bundled `dist/**` Jiti native-load policy and keep channel, public-surface, facade, and config-metadata loader seams off native Jiti on Windows so onboarding and configure flows stop tripping `ERR_UNSUPPORTED_ESM_URL_SCHEME`. (#62286) Thanks @chen-zhang-cs-code.
|
||||
- Plugins/channels: keep bundled channel artifact and secret-contract loading stable under lazy loading, preserve plugin-schema defaults during install, and fix Windows `file://` plus native-Jiti plugin loader paths so onboarding, doctor, `openclaw secret`, and bundled plugin installs work again. (#61832, #61836, #61853, #61856) Thanks @Zeesejo and contributors.
|
||||
- Plugins/ClawHub: verify downloaded plugin archives against version metadata SHA-256, fail closed when archive integrity metadata is missing or malformed, and tighten fallback ZIP verification so plugin installs cannot proceed on mismatched or incomplete ClawHub package metadata. (#60517) Thanks @mappel-nv.
|
||||
- Plugins/provider hooks: stop recursive provider snapshot loads from overflowing the stack during plugin initialization, while still preserving cached nested provider-hook results. (#61922, #61938, #61946, #61951)
|
||||
- Docker/plugins: stop forcing bundled plugin discovery to `/app/extensions` in runtime images so packaged installs use compiled `dist/extensions` artifacts again and Node 24 containers do not boot through source-only plugin entry paths. Fixes #62044. (#62316) Thanks @gumadeiras.
|
||||
- Cron: load `jobId` into `id` when the on-disk store omits `id`, matching doctor migration and fixing `unknown cron job id` for hand-edited `jobs.json`. (#62246) Thanks @neeravmakwana.
|
||||
- Agents/model fallback: classify minimal HTTP 404 API errors (for example `404 status code (no body)`) as `model_not_found` so assistant failures throw into the fallback chain instead of stopping at the first fallback candidate. (#62119) Thanks @neeravmakwana.
|
||||
- Providers/Ollama: honor the selected provider's `baseUrl` during streaming so multi-Ollama setups stop routing every stream to the first configured Ollama endpoint. (#61678)
|
||||
- Providers/Ollama: stop warning that Ollama could not be reached when discovery only sees empty default local stubs, while still keeping real explicit Ollama overrides loud when the endpoint is unreachable.
|
||||
- Providers/xAI: recognize `api.grok.x.ai` as an xAI-native endpoint again and keep legacy `x_search` auth resolution working so older xAI web-search configs continue to load. (#61377) Thanks @jjjojoj.
|
||||
- Providers/Mistral: send `reasoning_effort` for `mistral/mistral-small-latest` (Mistral Small 4) with thinking-level mapping, and mark the catalog entry as reasoning-capable so adjustable reasoning matches Mistral’s Chat Completions API. (#62162) Thanks @neeravmakwana.
|
||||
- OpenAI TTS/Groq: send `wav` to Groq-compatible speech endpoints, honor explicit `responseFormat` overrides on OpenAI-compatible paths, and only mark voice-note output as voice-compatible when the actual format is `opus`. (#62233) Thanks @neeravmakwana.
|
||||
- Tools/web_fetch and web_search: fix `TypeError: fetch failed` caused by undici 8.0 enabling HTTP/2 by default; pinned SSRF-guard dispatchers now explicitly set `allowH2: false` to restore HTTP/1.1 behavior and keep the custom DNS-pinning lookup compatible. (#61738, #61777) Thanks @zozo123.
|
||||
- Tools/web search/Exa: show Exa Search in onboarding and configure provider pickers again by marking the bundled Exa provider as setup-visible. Thanks @vincentkoc.
|
||||
- Memory/vector recall: surface explicit warnings when `sqlite-vec` is unavailable or vector writes are degraded, and strip managed Light Sleep and REM blocks before daily-note ingestion so memory indexing and dreaming stop reporting false-success or re-ingesting staged output. (#61720) Thanks @MonkeyLeeT.
|
||||
- Memory/dreaming: make Dreams config reads and writes respect the selected memory slot plugin instead of always targeting `memory-core`. (#62275) Thanks @SnowSky1.
|
||||
- QQ Bot/media: route gateway-side attachment and fallback downloads through guarded QQ/Tencent HTTPS fetches so QQ media handling no longer follows arbitrary remote hosts.
|
||||
- Browser/remote CDP: retry the DevTools websocket once after remote browser restarts so healthy remote browser profiles do not fail availability checks during CDP warm-up. (#57397) Thanks @ThanhNguyxn07.
|
||||
- UI/light mode: target both root and nested WebKit scrollbar thumbs in the light theme so page-level and container scrollbars stay visible on light backgrounds. (#61753) Thanks @chziyue.
|
||||
- Agents/subagents: honor `sessions_spawn(lightContext: true)` for spawned subagent runs by preserving lightweight bootstrap context through the gateway and embedded runner instead of silently falling back to full workspace bootstrap injection. (#62264) Thanks @theSamPadilla.
|
||||
- Cron: load `jobId` into `id` when the on-disk store omits `id`, matching doctor migration and fixing `unknown cron job id` for hand-edited `jobs.json`. (#62246) Thanks @neeravmakwana.
|
||||
- Agents/model fallback: classify minimal HTTP 404 API errors (for example `404 status code (no body)`) as `model_not_found` so assistant failures throw into the fallback chain instead of stopping at the first fallback candidate. (#62119) Thanks @neeravmakwana.
|
||||
- BlueBubbles/network: respect explicit private-network opt-out for loopback and private `serverUrl` values across account resolution, status probes, monitor startup, and attachment downloads, while keeping public-host attachment hostname pinning intact. (#59373) Thanks @jpreagan.
|
||||
- Agents/heartbeat: keep heartbeat runs pinned to the main session so active subagent transcripts are not overwritten by heartbeat status messages. (#61803) Thanks @100yenadmin.
|
||||
- Agents/heartbeat: respect disabled heartbeat prompt guidance so operators can suppress heartbeat prompt instructions without disabling heartbeat runtime behavior.
|
||||
- Agents/compaction: stop compaction-wait aborts from re-entering prompt failover and replaying completed tool turns. (#62600) Thanks @i-dentifier.
|
||||
- Approvals/runtime: move native approval lifecycle assembly into shared core bootstrap/runtime seams driven by channel capabilities and runtime contexts, and remove the legacy bundled approval fallback wiring. (#62135) Thanks @gumadeiras.
|
||||
- Security/fetch-guard: stop rejecting operator-configured proxy hostnames against the target-scoped hostname allowlist in SSRF-guarded fetches, restoring proxy-based media downloads for Telegram and other channels. (#62312) Thanks @ademczuk.
|
||||
- iOS/gateway: replace string-matched connection error UI with structured gateway connection problems, preserve actionable pairing/auth failures over later generic disconnect noise, and surface reusable problem banners and details across onboarding, settings, and root status surfaces. (#62650) Thanks @ngutman.
|
||||
- Logging: make `logging.level` and `logging.consoleLevel` honor the documented severity threshold ordering again, and keep child loggers inheriting the parent `minLevel`. (#44646) Thanks @zhumengzhu.
|
||||
- Agents/sessions_send: pass `threadId` through announce delivery so cross-session notifications land in the correct Telegram forum topic instead of the group's general thread. (#62758) Thanks @jalehman.
|
||||
- Daemon/systemd: keep sudo systemctl calls scoped to the invoking user when machine-scoped systemctl fails, while still avoiding machine fallback for permission-denied user bus errors. (#62337) Thanks @Aftabbs.
|
||||
- Docs/i18n: relocalize final localized-page links after translation and remove the zh-CN homepage redirect override so localized Mintlify pages resolve to the correct language roots again. (#61796) Thanks @hxy91819.
|
||||
- Agents/exec: keep timed-out shell-backgrounded commands on the failed path and point long-running jobs to exec background/yield sessions so process polling is only suggested for registered sessions.
|
||||
- Agents/model resolution: let explicit `openai-codex/gpt-5.4` selection prefer provider runtime metadata when it reports a larger context window, keeping configured Codex runs aligned with the live provider limits. (#62694) Thanks @ruclaw7.
|
||||
- Agents/model resolution: keep explicit-model runtime comparisons on the configured workspace plugin registry, so workspace-installed providers do not silently fall back to stale explicit metadata during runtime model lookup.
|
||||
- Providers/Z.AI: default onboarding and endpoint detection to GLM-5.1 instead of GLM-5. (#61998) Thanks @serg0x.
|
||||
- Reply execution: prefer the active runtime snapshot over stale queued reply config during embedded reply and follow-up execution so SecretRef-backed reply turns stop crashing after secrets have already resolved. (#62693) Thanks @mbelinky.
|
||||
|
||||
## 2026.4.5
|
||||
|
||||
@@ -1771,6 +1801,9 @@ Docs: https://docs.openclaw.ai
|
||||
- macOS overlays: fix VoiceWake, Talk, and Notify overlay exclusivity crashes by removing shared `inout` visibility mutation from `OverlayPanelFactory.present`, and add a repeated Talk overlay smoke test. (#39275, #39321) Thanks @fellanH.
|
||||
- macOS Talk Mode: set the speech recognition request `taskHint` to `.dictation` for mic capture, and add regression coverage for the request defaults. (#38445) Thanks @dmiv.
|
||||
- macOS release packaging: default `scripts/package-mac-app.sh` to universal binaries for `BUILD_CONFIG=release`, and clarify that `scripts/package-mac-dist.sh` already produces the release zip + DMG. (#33891) Thanks @cgdusek.
|
||||
- Tools/web search: restore Perplexity OpenRouter/Sonar compatibility for legacy `OPENROUTER_API_KEY`, `sk-or-...`, and explicit `perplexity.baseUrl` / `model` setups while keeping direct Perplexity keys on the native Search API path. (#39937) Thanks @obviyus.
|
||||
- Tools/web search: restore Perplexity OpenRouter/Sonar compatibility for legacy `OPENROUTER_API_KEY`, `sk-or-...`, and explicit `perplexity.baseUrl` / `model` setups while keeping direct Perplexity keys on the native Search API path. (#39937) Thanks @obviyus.
|
||||
- Doctor/Codex OAuth: warn only for legacy `models.providers.openai-codex` transport overrides that can shadow the built-in Codex OAuth path, while leaving supported custom proxies and header-only overrides alone. (#40143) Thanks @bde1.
|
||||
- Hooks/session-memory: keep `/new` and `/reset` memory artifacts in the bound agent workspace and align saved reset session keys with that workspace when stale main-agent keys leak into the hook path. (#39875) thanks @rbutera.
|
||||
- Sessions/model switch: clear stale cached `contextTokens` when a session changes models so status and runtime paths recompute against the active model window. (#38044) thanks @yuweuii.
|
||||
- ACP/session history: persist transcripts for successful ACP child runs, preserve exact transcript text, record ACP spawned-session lineage, and keep spawn-time transcript-path persistence best-effort so history storage failures do not block execution. (#40137) thanks @mbelinky.
|
||||
|
||||
317
appcast.xml
317
appcast.xml
@@ -2,6 +2,136 @@
|
||||
<rss xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" version="2.0">
|
||||
<channel>
|
||||
<title>OpenClaw</title>
|
||||
<item>
|
||||
<title>2026.4.8</title>
|
||||
<pubDate>Wed, 08 Apr 2026 06:12:50 +0000</pubDate>
|
||||
<link>https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml</link>
|
||||
<sparkle:version>2026040890</sparkle:version>
|
||||
<sparkle:shortVersionString>2026.4.8</sparkle:shortVersionString>
|
||||
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
|
||||
<description><![CDATA[<h2>OpenClaw 2026.4.8</h2>
|
||||
<h3>Fixes</h3>
|
||||
<ul>
|
||||
<li>Telegram/setup: load setup and secret contracts through packaged top-level sidecars so installed npm builds no longer try to import missing <code>dist/extensions/telegram/src/*</code> files during gateway startup.</li>
|
||||
<li>Bundled channels/setup: load shared secret contracts through packaged top-level sidecars across BlueBubbles, Feishu, Google Chat, IRC, Matrix, Mattermost, Microsoft Teams, Nextcloud Talk, Slack, and Zalo so installed npm builds no longer rely on missing <code>dist/extensions/*/src/*</code> files during gateway startup.</li>
|
||||
<li>Bundled plugins: align packaged plugin compatibility metadata with the release version so bundled channels and providers load on OpenClaw 2026.4.8.</li>
|
||||
<li>Agents/progress: keep <code>update_plan</code> available for OpenAI-family runs while returning compact success payloads and allowing <code>tools.experimental.planTool=false</code> to opt out.</li>
|
||||
<li>Agents/exec: keep <code>/exec</code> current-default reporting aligned with real runtime behavior so <code>host=auto</code> sessions surface the correct host-aware fallback policy (<code>full/off</code> on gateway or node, <code>deny/off</code> on sandbox) instead of stale stricter defaults.</li>
|
||||
<li>Slack: honor ambient HTTP(S) proxy settings for Socket Mode WebSocket connections, including NO_PROXY exclusions, so proxy-only deployments can connect without a monkey patch. (#62878) Thanks @mjamiv.</li>
|
||||
<li>Slack/actions: pass the already resolved read token into <code>downloadFile</code> so SecretRef-backed bot tokens no longer fail after a raw config re-read. (#62097) Thanks @martingarramon.</li>
|
||||
<li>Network/fetch guard: skip target DNS pinning when trusted env-proxy mode is active so proxy-only sandboxes can let the trusted proxy resolve outbound hosts. (#59007) Thanks @cluster2600.</li>
|
||||
</ul>
|
||||
<p><a href="https://github.com/openclaw/openclaw/blob/main/CHANGELOG.md">View full changelog</a></p>
|
||||
]]></description>
|
||||
<enclosure url="https://github.com/openclaw/openclaw/releases/download/v2026.4.8/OpenClaw-2026.4.8.zip" length="25324810" type="application/octet-stream" sparkle:edSignature="aogl3hJf+FeRvQj0W4WDGMQnIRPpxXPQam50U7SBT3ljA1CeSbIGsnaj20aLF0Qc9DikPEXt5AEg7LMOen4+BQ=="/>
|
||||
</item>
|
||||
<item>
|
||||
<title>2026.4.7</title>
|
||||
<pubDate>Wed, 08 Apr 2026 02:54:26 +0000</pubDate>
|
||||
<link>https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml</link>
|
||||
<sparkle:version>2026040790</sparkle:version>
|
||||
<sparkle:shortVersionString>2026.4.7</sparkle:shortVersionString>
|
||||
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
|
||||
<description><![CDATA[<h2>OpenClaw 2026.4.7</h2>
|
||||
<h3>Changes</h3>
|
||||
<ul>
|
||||
<li>CLI/infer: add a first-class <code>openclaw infer ...</code> hub for provider-backed inference workflows across model, media, web, and embedding tasks. Thanks @Takhoffman.</li>
|
||||
<li>Tools/media generation: auto-fallback across auth-backed image, music, and video providers by default, preserve intent during provider switches, remap size/aspect/resolution/duration hints to the closest supported option, and surface provider capabilities plus mode-aware video-to-video support.</li>
|
||||
<li>Memory/wiki: restore the bundled <code>memory-wiki</code> stack with plugin, CLI, sync/query/apply tooling, memory-host integration, structured claim/evidence fields, compiled digest retrieval, claim-health linting, contradiction clustering, staleness dashboards, and freshness-weighted search. Thanks @vincentkoc.</li>
|
||||
<li>Plugins/webhooks: add a bundled webhook ingress plugin so external automation can create and drive bound TaskFlows through per-route shared-secret endpoints. (#61892) Thanks @mbelinky.</li>
|
||||
<li>Gateway/sessions: add persisted compaction checkpoints plus Sessions UI branch/restore actions so operators can inspect and recover pre-compaction session state. (#62146) Thanks @scoootscooob.</li>
|
||||
<li>Compaction: add pluggable compaction provider registry so plugins can replace the built-in summarization pipeline. Configure via <code>agents.defaults.compaction.provider</code>; falls back to LLM summarization on provider failure. (#56224) Thanks @DhruvBhatia0.</li>
|
||||
<li>Agents/system prompt: add <code>agents.defaults.systemPromptOverride</code> for controlled prompt experiments plus heartbeat prompt-section controls so heartbeat runtime behavior can stay enabled without injecting heartbeat instructions every turn.</li>
|
||||
<li>Providers/Google: add Gemma 4 model support and keep Google fallback resolution on the requested provider path so native Google Gemma routes work again. (#61507) Thanks @eyjohn.</li>
|
||||
<li>Providers/Google: preserve explicit thinking-off semantics for Gemma 4 while still enabling Gemma reasoning support in compatibility wrappers. (#62127) Thanks @romgenie.</li>
|
||||
<li>Providers/Arcee AI: add a bundled Arcee AI provider plugin with Trinity catalog entries, OpenRouter support, and updated onboarding/auth guidance. (#62068) Thanks @arthurbr11.</li>
|
||||
<li>Providers/Anthropic: restore Claude CLI as the preferred local Anthropic path in onboarding, model-auth guidance, doctor flows, and Docker Claude CLI live lanes again.</li>
|
||||
<li>Providers/Ollama: detect vision capability from the <code>/api/show</code> response and set image input on models that support it so Ollama vision models accept image attachments. (#62193) Thanks @BruceMacD.</li>
|
||||
<li>Memory/dreaming: ingest redacted session transcripts into the dreaming corpus with per-day session-corpus notes, cursor checkpointing, and promotion/doctor support. (#62227) Thanks @vignesh07.</li>
|
||||
<li>Providers/inferrs: add string-content compatibility for stricter OpenAI-compatible chat backends, document <code>inferrs</code> setup with a full config example, and add troubleshooting guidance for local backends that pass direct probes but fail on full agent-runtime prompts.</li>
|
||||
<li>Agents/context engine: expose prompt-cache runtime context to context engines and keep current-turn prompt-cache usage aligned with the active attempt instead of stale prior-turn assistant state. (#62179) Thanks @jalehman.</li>
|
||||
<li>Plugin SDK/context engines: pass <code>availableTools</code> and <code>citationsMode</code> into <code>assemble()</code>, and expose memory-artifact and memory-prompt seams so companion plugins and non-legacy context engines can consume active memory state without reaching into internals. Thanks @vincentkoc.</li>
|
||||
<li>ACP/ACPX plugin: bump the bundled <code>acpx</code> pin to <code>0.5.1</code> so plugin-local installs and strict version checks pick up the latest published runtime release. (#62148) Thanks @onutc.</li>
|
||||
<li>Discord/events: allow <code>event-create</code> to accept a cover image URL or local file path, load and validate PNG/JPG/GIF event cover media, and pass the encoded image payload through Discord admin action/runtime paths. (#60883) Thanks @bittoby.</li>
|
||||
</ul>
|
||||
<h3>Fixes</h3>
|
||||
<ul>
|
||||
<li>CLI/infer: keep provider-backed infer behavior aligned with actual runtime execution by fixing explicit TTS override handling, profile-aware gateway TTS prefs resolution, per-request transcription <code>prompt</code>/<code>language</code> overrides, image output MIME/extension mismatches, configured web-search fallback behavior, and agent-vs-CLI web-search execution drift.</li>
|
||||
<li>Plugins/media: when <code>plugins.allow</code> is set, capability fallback now merges bundled capability plugin ids into the allowlist (not only <code>plugins.entries</code>), so media understanding providers such as OpenAI-compatible STT load for voice transcription without requiring <code>openai</code> in <code>plugins.allow</code>. (#62205) Thanks @neeravmakwana.</li>
|
||||
<li>Agents/history and replies: buffer phaseless OpenAI WS text until a real assistant phase arrives, keep replay and SSE history sequence tracking aligned, hide commentary and leaked tool XML from user-visible history, and keep history-based follow-up replies on <code>final_answer</code> text only. (#61729, #61747, #61829, #61855, #61954) Thanks @100yenadmin and contributors.</li>
|
||||
<li>Control UI: show <code>/tts</code> audio replies in webchat, detect mistaken <code>?token=</code> auth links with the correct <code>#token=</code> hint, and keep Copy, Canvas, and mobile exec-approval UI from covering chat content on narrow screens. (#54842, #61514, #61598) Thanks @neeravmakwana.</li>
|
||||
<li>iOS/gateway: replace string-matched connection error UI with structured gateway connection problems, preserve actionable pairing/auth failures over later generic disconnect noise, and surface reusable problem banners and details across onboarding, settings, and root status surfaces. (#62650) Thanks @ngutman.</li>
|
||||
<li>TUI: route <code>/status</code> through the shared session-status command, keep commentary hidden in history, strip raw envelope metadata from async command notices, preserve fallback streaming before per-attempt failures finalize, and restore Kitty keyboard state on exit or fatal crashes. (#49130, #59985, #60043, #61463) Thanks @biefan and contributors.</li>
|
||||
<li>iOS/Watch exec approvals: keep Apple Watch review and approval recovery working while the iPhone is locked or backgrounded, including reconnect recovery, pending approval persistence, notification cleanup, and APNs-backed watch refresh recovery. (#61757) Thanks @ngutman.</li>
|
||||
<li>Agents/context overflow: combine oversized and aggregate tool-result recovery in one pass and restore a total-context overflow backstop so recoverable sessions retry instead of failing early. (#61651) Thanks @Takhoffman.</li>
|
||||
<li>Auth/OpenAI Codex OAuth: reload fresh on-disk credentials inside the locked refresh path and retry once after <code>refresh_token_reused</code> rotates only the stored refresh token, so relogin/restart recovery stops getting stuck on stale cached auth state. Thanks @owen-ever.</li>
|
||||
<li>Auth/OpenAI Codex OAuth: keep native <code>/model ...@profile</code> selections on the target session and honor explicit user-locked auth profiles even when per-agent auth order excludes them. (#62744) Thanks @jalehman.</li>
|
||||
<li>Providers/Anthropic: preserve thinking blocks for Claude Opus 4.5+, Sonnet 4.5+, and newer Claude 4-family models so prompt-cache prefixes keep matching, and skip <code>service_tier</code> injection on OAuth-authenticated stream wrapper requests so Claude OAuth streaming stops failing with HTTP 401. (#60356, #61793)</li>
|
||||
<li>Agents/Claude CLI: surface nested API error messages from structured CLI output so billing/auth/provider failures show the real provider error instead of an opaque CLI failure.</li>
|
||||
<li>Agents/exec: preserve explicit <code>host=node</code> routing under elevated defaults when <code>tools.exec.host=auto</code>, fail loud on invalid elevated cross-host overrides, and keep <code>strictInlineEval</code> commands blocked after approval timeouts instead of falling through to automatic execution. (#61739) Thanks @obviyus.</li>
|
||||
<li>Nodes/exec approvals: keep <code>host=node</code> POSIX transport shell wrappers (<code>/bin/sh -lc ...</code>) aligned with inner-command allowlist analysis so allowlisted scripts stop prompting unnecessarily, while Windows <code>cmd.exe</code> wrapper runs stay approval-gated. (#62401) Thanks @ngutman.</li>
|
||||
<li>Nodes/exec approvals: keep Windows <code>cmd.exe /c</code> wrapper runs approval-gated even when <code>env</code> carriers, including env-assignment carriers, wrap the shell invocation. (#62439) Thanks @ngutman.</li>
|
||||
<li>Gateway tool/exec config: block model-facing <code>gateway config.apply</code> and <code>config.patch</code> writes from changing exec approval paths such as <code>safeBins</code>, <code>safeBinProfiles</code>, <code>safeBinTrustedDirs</code>, and <code>strictInlineEval</code>, while still allowing unchanged structured values through. (#62001) Thanks @eleqtrizit.</li>
|
||||
<li>Host exec/env sanitization: block dangerous Java, Rust, Cargo, Git, Kubernetes, cloud credential, config-path, and Helm env overrides so host-run tools cannot be redirected to attacker-chosen code, config, credentials, or repository state. (#59119, #62002, #62291) Thanks @eleqtrizit and contributors.</li>
|
||||
<li>Commands/allowlist: require owner authorization for <code>/allowlist add</code> and <code>/allowlist remove</code> before channel resolution, so non-owner but command-authorized senders can no longer persistently rewrite allowlist policy state. (#62383) Thanks @pgondhi987.</li>
|
||||
<li>Feishu/docx uploads: honor <code>tools.fs.workspaceOnly</code> for local <code>upload_file</code> and <code>upload_image</code> paths by forwarding workspace-constrained <code>localRoots</code> into the media loader, so docx uploads can no longer read host-local files outside the workspace when workspace-only mode is active. (#62369) Thanks @pgondhi987.</li>
|
||||
<li>Network/fetch guard: drop request bodies and body-describing headers on cross-origin <code>307</code> and <code>308</code> redirects by default, so attacker-controlled redirect hops cannot receive secret-bearing POST payloads from SSRF-guarded fetch flows unless a caller explicitly opts in. (#62357) Thanks @pgondhi987.</li>
|
||||
<li>Browser/SSRF: treat main-frame <code>document</code> redirect hops as navigations even when Playwright does not flag them as <code>isNavigationRequest()</code>, so strict private-network blocking still stops forbidden redirect pivots before the browser reaches the internal target. (#62355) Thanks @pgondhi987.</li>
|
||||
<li>Browser/node invoke: block persistent browser profile create, reset, and delete mutations through <code>browser.proxy</code> on both gateway-forwarded <code>node.invoke</code> and the node-host proxy path, even when no profile allowlist is configured. (#60489)</li>
|
||||
<li>Gateway/node pairing: require a fresh pairing request when a previously paired node reconnects with additional declared commands, and keep the live session pinned to the earlier approved command set until the upgrade is approved. (#62658) Thanks @eleqtrizit.</li>
|
||||
<li>Gateway/auth: invalidate existing shared-token and password WebSocket sessions when the configured secret rotates, so stale authenticated sockets cannot stay attached after token or password changes. (#62350) Thanks @pgondhi987.</li>
|
||||
<li>MS Teams/security: validate file-consent upload URLs against HTTPS, Microsoft/SharePoint host allowlists, and private-IP DNS checks before uploading attachments, blocking SSRF-style consent-upload abuse. (#23596)</li>
|
||||
<li>Media/base64 decode guards: enforce byte limits before decoding missed base64-backed Teams, Signal, QQ Bot, and image-tool payloads so oversized inbound media and data URLs no longer bypass pre-decode size checks. (#62007) Thanks @eleqtrizit.</li>
|
||||
<li>Runtime event trust: mark background <code>notifyOnExit</code> summaries, ACP parent-stream relays, and wake-hook payloads as untrusted system events so lower-trust runtime output no longer re-enters later turns as trusted <code>System:</code> text. (#62003)</li>
|
||||
<li>Auto-reply/media: allow managed generated-media <code>MEDIA:</code> paths from normal reply text again while still blocking arbitrary host-local media and document paths, so generated media keep delivering without reopening host-path injection holes.</li>
|
||||
<li>Gateway/status and containers: auto-bind to <code>0.0.0.0</code> inside Docker and Podman environments, and probe local TLS gateways over <code>wss://</code> with self-signed fingerprint forwarding so container startup and loopback TLS status checks work again. (#61818, #61935) Thanks @openperf and contributors.</li>
|
||||
<li>Gateway/OpenAI-compatible HTTP: abort in-flight <code>/v1/chat/completions</code> and <code>/v1/responses</code> turns when clients disconnect so abandoned HTTP requests stop wasting agent runtime. (#54388) Thanks @Lellansin.</li>
|
||||
<li>macOS/gateway version: strip trailing commit metadata from CLI version output before semver parsing so the Mac app recognizes installed gateway versions like <code>OpenClaw 2026.4.2 (d74a122)</code> again. (#61111) Thanks @oliviareid-svg.</li>
|
||||
<li>Sessions/model selection: resolve the explicitly selected session model separately from runtime fallback resolution so session status and live model switching stay aligned with the chosen model.</li>
|
||||
<li>Discord/ACP bindings: canonicalize DM conversation identity across inbound messages, component interactions, native commands, and current-conversation binding resolution so <code>--bind here</code> in Discord DMs keeps routing follow-up replies to the bound agent instead of falling back to the default agent.</li>
|
||||
<li>Discord: recover forwarded referenced message text and attachments when snapshots are missing, use <code>ws://</code> again for gateway monitor sockets, stop forcing a hardcoded temperature for Codex-backed auto-thread titles, and harden voice receive recovery so rapid speaker restarts keep their next utterance. (#41536, #61670) Thanks @artwalker and contributors.</li>
|
||||
<li>Slack/thread mentions: add <code>channels.slack.thread.requireExplicitMention</code> so Slack channels that already require mentions can also require explicit <code>@bot</code> mentions inside bot-participated threads. (#58276) Thanks @praktika-engineer.</li>
|
||||
<li>Slack/threading: keep legacy thread stickiness for real replies when older callers omit <code>isThreadReply</code>, while still honoring <code>replyToMode</code> for Slack's auto-created top-level <code>thread_ts</code>. (#61835) Thanks @kaonash.</li>
|
||||
<li>Slack/media: keep attachment downloads on the SSRF-guarded dispatcher path so Slack media fetching works on Node 22 without dropping pinned transport enforcement. (#62239) Thanks @openperf.</li>
|
||||
<li>Matrix/onboarding: add an invite auto-join setup step with explicit off warnings and strict stable-target validation so new Matrix accounts stop silently ignoring invited rooms and fresh DM-style invites unless operators opt in. (#62168) Thanks @gumadeiras.</li>
|
||||
<li>Matrix/formatting: preserve multi-paragraph and loose-list rendering in Element so numbered and bulleted Markdown keeps their content attached to the correct list item. (#60997) Thanks @gucasbrg.</li>
|
||||
<li>Telegram/doctor: keep top-level access-control fallback in place during multi-account normalization while still promoting legacy default auth into <code>accounts.default</code>, so existing named bots keep inherited allowlists without dropping the legacy default bot. (#62263) Thanks @obviyus.</li>
|
||||
<li>Plugins/loaders: centralize bundled <code>dist/**</code> Jiti native-load policy and keep channel, public-surface, facade, and config-metadata loader seams off native Jiti on Windows so onboarding and configure flows stop tripping <code>ERR_UNSUPPORTED_ESM_URL_SCHEME</code>. (#62286) Thanks @chen-zhang-cs-code.</li>
|
||||
<li>Plugins/channels: keep bundled channel artifact and secret-contract loading stable under lazy loading, preserve plugin-schema defaults during install, and fix Windows <code>file://</code> plus native-Jiti plugin loader paths so onboarding, doctor, <code>openclaw secret</code>, and bundled plugin installs work again. (#61832, #61836, #61853, #61856) Thanks @Zeesejo and contributors.</li>
|
||||
<li>Plugins/ClawHub: verify downloaded plugin archives against version metadata SHA-256, fail closed when archive integrity metadata is missing or malformed, and tighten fallback ZIP verification so plugin installs cannot proceed on mismatched or incomplete ClawHub package metadata. (#60517) Thanks @mappel-nv.</li>
|
||||
<li>Plugins/provider hooks: stop recursive provider snapshot loads from overflowing the stack during plugin initialization, while still preserving cached nested provider-hook results. (#61922, #61938, #61946, #61951)</li>
|
||||
<li>Docker/plugins: stop forcing bundled plugin discovery to <code>/app/extensions</code> in runtime images so packaged installs use compiled <code>dist/extensions</code> artifacts again and Node 24 containers do not boot through source-only plugin entry paths. Fixes #62044. (#62316) Thanks @gumadeiras.</li>
|
||||
<li>Providers/Ollama: honor the selected provider's <code>baseUrl</code> during streaming so multi-Ollama setups stop routing every stream to the first configured Ollama endpoint. (#61678)</li>
|
||||
<li>Providers/Ollama: stop warning that Ollama could not be reached when discovery only sees empty default local stubs, while still keeping real explicit Ollama overrides loud when the endpoint is unreachable.</li>
|
||||
<li>Providers/xAI: recognize <code>api.grok.x.ai</code> as an xAI-native endpoint again and keep legacy <code>x_search</code> auth resolution working so older xAI web-search configs continue to load. (#61377) Thanks @jjjojoj.</li>
|
||||
<li>Providers/Mistral: send <code>reasoning_effort</code> for <code>mistral/mistral-small-latest</code> (Mistral Small 4) with thinking-level mapping, and mark the catalog entry as reasoning-capable so adjustable reasoning matches Mistral’s Chat Completions API. (#62162) Thanks @neeravmakwana.</li>
|
||||
<li>OpenAI TTS/Groq: send <code>wav</code> to Groq-compatible speech endpoints, honor explicit <code>responseFormat</code> overrides on OpenAI-compatible paths, and only mark voice-note output as voice-compatible when the actual format is <code>opus</code>. (#62233) Thanks @neeravmakwana.</li>
|
||||
<li>Tools/web_fetch and web_search: fix <code>TypeError: fetch failed</code> caused by undici 8.0 enabling HTTP/2 by default; pinned SSRF-guard dispatchers now explicitly set <code>allowH2: false</code> to restore HTTP/1.1 behavior and keep the custom DNS-pinning lookup compatible. (#61738, #61777) Thanks @zozo123.</li>
|
||||
<li>Tools/web search/Exa: show Exa Search in onboarding and configure provider pickers again by marking the bundled Exa provider as setup-visible. Thanks @vincentkoc.</li>
|
||||
<li>Memory/vector recall: surface explicit warnings when <code>sqlite-vec</code> is unavailable or vector writes are degraded, and strip managed Light Sleep and REM blocks before daily-note ingestion so memory indexing and dreaming stop reporting false-success or re-ingesting staged output. (#61720) Thanks @MonkeyLeeT.</li>
|
||||
<li>Memory/dreaming: make Dreams config reads and writes respect the selected memory slot plugin instead of always targeting <code>memory-core</code>. (#62275) Thanks @SnowSky1.</li>
|
||||
<li>QQ Bot/media: route gateway-side attachment and fallback downloads through guarded QQ/Tencent HTTPS fetches so QQ media handling no longer follows arbitrary remote hosts.</li>
|
||||
<li>Browser/remote CDP: retry the DevTools websocket once after remote browser restarts so healthy remote browser profiles do not fail availability checks during CDP warm-up. (#57397) Thanks @ThanhNguyxn07.</li>
|
||||
<li>UI/light mode: target both root and nested WebKit scrollbar thumbs in the light theme so page-level and container scrollbars stay visible on light backgrounds. (#61753) Thanks @chziyue.</li>
|
||||
<li>Agents/subagents: honor <code>sessions_spawn(lightContext: true)</code> for spawned subagent runs by preserving lightweight bootstrap context through the gateway and embedded runner instead of silently falling back to full workspace bootstrap injection. (#62264) Thanks @theSamPadilla.</li>
|
||||
<li>Cron: load <code>jobId</code> into <code>id</code> when the on-disk store omits <code>id</code>, matching doctor migration and fixing <code>unknown cron job id</code> for hand-edited <code>jobs.json</code>. (#62246) Thanks @neeravmakwana.</li>
|
||||
<li>Agents/model fallback: classify minimal HTTP 404 API errors (for example <code>404 status code (no body)</code>) as <code>model_not_found</code> so assistant failures throw into the fallback chain instead of stopping at the first fallback candidate. (#62119) Thanks @neeravmakwana.</li>
|
||||
<li>BlueBubbles/network: respect explicit private-network opt-out for loopback and private <code>serverUrl</code> values across account resolution, status probes, monitor startup, and attachment downloads, while keeping public-host attachment hostname pinning intact. (#59373) Thanks @jpreagan.</li>
|
||||
<li>Agents/heartbeat: keep heartbeat runs pinned to the main session so active subagent transcripts are not overwritten by heartbeat status messages. (#61803) Thanks @100yenadmin.</li>
|
||||
<li>Agents/heartbeat: respect disabled heartbeat prompt guidance so operators can suppress heartbeat prompt instructions without disabling heartbeat runtime behavior.</li>
|
||||
<li>Agents/compaction: stop compaction-wait aborts from re-entering prompt failover and replaying completed tool turns. (#62600) Thanks @i-dentifier.</li>
|
||||
<li>Approvals/runtime: move native approval lifecycle assembly into shared core bootstrap/runtime seams driven by channel capabilities and runtime contexts, and remove the legacy bundled approval fallback wiring. (#62135) Thanks @gumadeiras.</li>
|
||||
<li>Security/fetch-guard: stop rejecting operator-configured proxy hostnames against the target-scoped hostname allowlist in SSRF-guarded fetches, restoring proxy-based media downloads for Telegram and other channels. (#62312) Thanks @ademczuk.</li>
|
||||
<li>Logging: make <code>logging.level</code> and <code>logging.consoleLevel</code> honor the documented severity threshold ordering again, and keep child loggers inheriting the parent <code>minLevel</code>. (#44646) Thanks @zhumengzhu.</li>
|
||||
<li>Agents/sessions_send: pass <code>threadId</code> through announce delivery so cross-session notifications land in the correct Telegram forum topic instead of the group's general thread. (#62758) Thanks @jalehman.</li>
|
||||
<li>Daemon/systemd: keep sudo systemctl calls scoped to the invoking user when machine-scoped systemctl fails, while still avoiding machine fallback for permission-denied user bus errors. (#62337) Thanks @Aftabbs.</li>
|
||||
<li>Docs/i18n: relocalize final localized-page links after translation and remove the zh-CN homepage redirect override so localized Mintlify pages resolve to the correct language roots again. (#61796) Thanks @hxy91819.</li>
|
||||
<li>Agents/exec: keep timed-out shell-backgrounded commands on the failed path and point long-running jobs to exec background/yield sessions so process polling is only suggested for registered sessions.</li>
|
||||
</ul>
|
||||
<p><a href="https://github.com/openclaw/openclaw/blob/main/CHANGELOG.md">View full changelog</a></p>
|
||||
]]></description>
|
||||
<enclosure url="https://github.com/openclaw/openclaw/releases/download/v2026.4.7/OpenClaw-2026.4.7.zip" length="25324827" type="application/octet-stream" sparkle:edSignature="RyFWRz1trE/qvOiInD4vR6je9wx7fUTtHpZ94W8rMlZDByux9CyXOm/Anai96b9KyjTeQyC7YnJp5SRnYY3iCg=="/>
|
||||
</item>
|
||||
<item>
|
||||
<title>2026.4.5</title>
|
||||
<pubDate>Mon, 06 Apr 2026 04:55:17 +0100</pubDate>
|
||||
@@ -250,190 +380,5 @@
|
||||
]]></description>
|
||||
<enclosure url="https://github.com/openclaw/openclaw/releases/download/v2026.4.5/OpenClaw-2026.4.5.zip" length="25050620" type="application/octet-stream" sparkle:edSignature="gVbB/73byllY0utwGIi3P5t0FyvLldeR0Uq2pAa6LTBr8VyZlwNCZ2xPlt2zDFshSUBFKxicYzohOmfJ28ACBg=="/>
|
||||
</item>
|
||||
<item>
|
||||
<title>2026.4.2</title>
|
||||
<pubDate>Thu, 02 Apr 2026 18:57:54 +0000</pubDate>
|
||||
<link>https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml</link>
|
||||
<sparkle:version>2026040290</sparkle:version>
|
||||
<sparkle:shortVersionString>2026.4.2</sparkle:shortVersionString>
|
||||
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
|
||||
<description><![CDATA[<h2>OpenClaw 2026.4.2</h2>
|
||||
<h3>Breaking</h3>
|
||||
<ul>
|
||||
<li>Plugins/xAI: move <code>x_search</code> settings from the legacy core <code>tools.web.x_search.*</code> path to the plugin-owned <code>plugins.entries.xai.config.xSearch.*</code> path, standardize <code>x_search</code> auth on <code>plugins.entries.xai.config.webSearch.apiKey</code> / <code>XAI_API_KEY</code>, and migrate legacy config with <code>openclaw doctor --fix</code>. (#59674) Thanks @vincentkoc.</li>
|
||||
<li>Plugins/web fetch: move Firecrawl <code>web_fetch</code> config from the legacy core <code>tools.web.fetch.firecrawl.*</code> path to the plugin-owned <code>plugins.entries.firecrawl.config.webFetch.*</code> path, route <code>web_fetch</code> fallback through the new fetch-provider boundary instead of a Firecrawl-only core branch, and migrate legacy config with <code>openclaw doctor --fix</code>. (#59465) Thanks @vincentkoc.</li>
|
||||
</ul>
|
||||
<h3>Changes</h3>
|
||||
<ul>
|
||||
<li>Tasks/Task Flow: restore the core Task Flow substrate with managed-vs-mirrored sync modes, durable flow state/revision tracking, and <code>openclaw flows</code> inspection/recovery primitives so background orchestration can persist and be operated separately from plugin authoring layers. (#58930) Thanks @mbelinky.</li>
|
||||
<li>Tasks/Task Flow: add managed child task spawning plus sticky cancel intent, so external orchestrators can stop scheduling immediately and let parent Task Flows settle to <code>cancelled</code> once active child tasks finish. (#59610) Thanks @mbelinky.</li>
|
||||
<li>Plugins/Task Flow: add a bound <code>api.runtime.taskFlow</code> seam so plugins and trusted authoring layers can create and drive managed Task Flows from host-resolved OpenClaw context without passing owner identifiers on each call. (#59622) Thanks @mbelinky.</li>
|
||||
<li>Android/assistant: add assistant-role entrypoints plus Google Assistant App Actions metadata so Android can launch OpenClaw from the assistant trigger and hand prompts into the chat composer. (#59596) Thanks @obviyus.</li>
|
||||
<li>Exec defaults: make gateway/node host exec default to YOLO mode by requesting <code>security=full</code> with <code>ask=off</code>, and align host approval-file fallbacks plus docs/doctor reporting with that no-prompt default.</li>
|
||||
<li>Providers/runtime: add provider-owned replay hook surfaces for transcript policy, replay cleanup, and reasoning-mode dispatch. (#59143) Thanks @jalehman.</li>
|
||||
<li>Plugins/hooks: add <code>before_agent_reply</code> so plugins can short-circuit the LLM with synthetic replies after inline actions. (#20067) Thanks @JoshuaLelon.</li>
|
||||
<li>Channels/session routing: move provider-specific session conversation grammar into plugin-owned session-key surfaces, preserving Telegram topic routing and Feishu scoped inheritance across bootstrap, model override, restart, and tool-policy paths.</li>
|
||||
<li>Feishu/comments: add a dedicated Drive comment-event flow with comment-thread context resolution, in-thread replies, and <code>feishu_drive</code> comment actions for document collaboration workflows. (#58497) Thanks @wittam-01.</li>
|
||||
<li>Matrix/plugin: emit spec-compliant <code>m.mentions</code> metadata across text sends, media captions, edits, poll fallback text, and action-driven edits so Matrix mentions notify reliably in clients like Element. (#59323) Thanks @gumadeiras.</li>
|
||||
<li>Diffs: add plugin-owned <code>viewerBaseUrl</code> so viewer links can use a stable proxy/public origin without passing <code>baseUrl</code> on every tool call. (#59341) Related #59227. Thanks @gumadeiras.</li>
|
||||
<li>Agents/compaction: resolve <code>agents.defaults.compaction.model</code> consistently for manual <code>/compact</code> and other context-engine compaction paths, so engine-owned compaction uses the configured override model across runtime entrypoints. (#56710) Thanks @oliviareid-svg.</li>
|
||||
<li>Agents/compaction: add <code>agents.defaults.compaction.notifyUser</code> so the <code>🧹 Compacting context...</code> start notice is opt-in instead of always being shown. (#54251) Thanks @oguricap0327.</li>
|
||||
<li>WhatsApp/reactions: add <code>reactionLevel</code> guidance for agent reactions. Thanks @mcaxtr.</li>
|
||||
<li>Exec approvals/channels: auto-enable DM-first native chat approvals when supported channels can infer approvers from existing owner config, while keeping channel fanout explicit and clarifying forwarding versus native approval client config.</li>
|
||||
</ul>
|
||||
<h3>Fixes</h3>
|
||||
<ul>
|
||||
<li>Providers/transport policy: centralize request auth, proxy, TLS, and header shaping across shared HTTP, stream, and websocket paths, block insecure TLS/runtime transport overrides, and keep proxy-hop TLS separate from target mTLS settings. (#59682) Thanks @vincentkoc.</li>
|
||||
<li>Providers/Copilot: classify native GitHub Copilot API hosts in the shared provider endpoint resolver and harden token-derived proxy endpoint parsing so Copilot base URL routing stays centralized and fails closed on malformed hints. (#59644) Thanks @vincentkoc.</li>
|
||||
<li>Providers/streaming headers: centralize default and attribution header merging across OpenAI websocket, embedded-runner, and proxy stream paths so provider-specific headers stay consistent and caller overrides only win where intended. (#59542) Thanks @vincentkoc.</li>
|
||||
<li>Providers/media HTTP: centralize base URL normalization, default auth/header injection, and explicit header override handling across shared OpenAI-compatible audio, Deepgram audio, Gemini media/image, and Moonshot video request paths. (#59469) Thanks @vincentkoc.</li>
|
||||
<li>Providers/OpenAI-compatible routing: centralize native-vs-proxy request policy so hidden attribution and related OpenAI-family defaults only apply on verified native endpoints across stream, websocket, and shared audio HTTP paths. (#59433) Thanks @vincentkoc.</li>
|
||||
<li>Providers/Anthropic routing: centralize native-vs-proxy endpoint classification for direct Anthropic <code>service_tier</code> handling so spoofed or proxied hosts do not inherit native Anthropic defaults. (#59608) Thanks @vincentkoc.</li>
|
||||
<li>Gateway/exec loopback: restore legacy-role fallback for empty paired-device token maps and allow silent local role upgrades so local exec and node clients stop failing with pairing-required errors after <code>2026.3.31</code>. (#59092) Thanks @openperf.</li>
|
||||
<li>Agents/subagents: pin admin-only subagent gateway calls to <code>operator.admin</code> while keeping <code>agent</code> at least privilege, so <code>sessions_spawn</code> no longer dies on loopback scope-upgrade pairing with <code>close(1008) "pairing required"</code>. (#59555) Thanks @openperf.</li>
|
||||
<li>Exec approvals/config: strip invalid <code>security</code>, <code>ask</code>, and <code>askFallback</code> values from <code>~/.openclaw/exec-approvals.json</code> during normalization so malformed policy enums fall back cleanly to the documented defaults instead of corrupting runtime policy resolution. (#59112) Thanks @openperf.</li>
|
||||
<li>Exec approvals/doctor: report host policy sources from the real approvals file path and ignore malformed host override values when attributing effective policy conflicts. (#59367) Thanks @gumadeiras.</li>
|
||||
<li>Exec/runtime: treat <code>tools.exec.host=auto</code> as routing-only, keep implicit no-config exec on sandbox when available or gateway otherwise, and reject per-call host overrides that would bypass the configured sandbox or host target. (#58897) Thanks @vincentkoc.</li>
|
||||
<li>Slack/mrkdwn formatting: add built-in Slack mrkdwn guidance in inbound context so Slack replies stop falling back to generic Markdown patterns that render poorly in Slack. (#59100) Thanks @jadewon.</li>
|
||||
<li>WhatsApp/presence: send <code>unavailable</code> presence on connect in self-chat mode so personal-phone users stop losing all push notifications while the gateway is running. (#59410) Thanks @mcaxtr.</li>
|
||||
<li>WhatsApp/media: add HTML, XML, and CSS to the MIME map and fall back gracefully for unknown media types instead of dropping the attachment. (#51562) Thanks @bobbyt74.</li>
|
||||
<li>Matrix/onboarding: restore guided setup in <code>openclaw channels add</code> and <code>openclaw configure --section channels</code>, while keeping custom plugin wizards on the shared <code>setupWizard</code> seam. (#59462) Thanks @gumadeiras.</li>
|
||||
<li>Matrix/streaming: keep live partial previews for the current assistant block while preserving completed block updates as separate messages when <code>channels.matrix.blockStreaming</code> is enabled. (#59384) Thanks @gumadeiras.</li>
|
||||
<li>Feishu/comment threads: harden document comment-thread delivery so whole-document comments fall back to <code>add_comment</code>, delayed reply lookups retry more reliably, and user-visible replies avoid reasoning/planning spillover. (#59129) Thanks @wittam-01.</li>
|
||||
<li>MS Teams/streaming: strip already-streamed text from fallback block delivery when replies exceed the 4000-character streaming limit so long responses stop duplicating content. (#59297) Thanks @bradgroux.</li>
|
||||
<li>Slack/thread context: filter thread starter and history by the effective conversation allowlist without dropping valid open-room, DM, or group DM context. (#58380) Thanks @jacobtomlinson.</li>
|
||||
<li>Mattermost/probes: route status probes through the SSRF guard and honor <code>allowPrivateNetwork</code> so connectivity checks stay safe for self-hosted Mattermost deployments. (#58529) Thanks @mappel-nv.</li>
|
||||
<li>Zalo/webhook replay: scope replay dedupe key by chat and sender so reused message IDs across different chats or senders no longer collide, and harden metadata reads for partially missing payloads. (#58444)</li>
|
||||
<li>QQBot/structured payloads: restrict local file paths to QQ Bot-owned media storage, block traversal outside that root, reduce path leakage in logs, and keep inline image data URLs working. (#58453) Thanks @jacobtomlinson.</li>
|
||||
<li>Image generation/providers: route OpenAI, MiniMax, and fal image requests through the shared provider HTTP transport path so custom base URLs, guarded private-network routing, and provider request defaults stay aligned with the rest of provider HTTP. Thanks @vincentkoc.</li>
|
||||
<li>Image generation/providers: stop inferring private-network access from configured OpenAI, MiniMax, and fal image base URLs, and cap shared HTTP error-body reads so hostile or misconfigured endpoints fail closed without relaxing SSRF policy or buffering unbounded error payloads. Thanks @vincentkoc.</li>
|
||||
<li>Browser/host inspection: keep static Chrome inspection helpers out of the activated browser runtime so <code>openclaw doctor browser</code> and related checks do not eagerly load the bundled browser plugin. (#59471) Thanks @vincentkoc.</li>
|
||||
<li>Browser/CDP: normalize trailing-dot localhost absolute-form hosts before loopback checks so remote CDP websocket URLs like <code>ws://localhost.:...</code> rewrite back to the configured remote host. (#59236) Thanks @mappel-nv.</li>
|
||||
<li>Agents/output sanitization: strip namespaced <code>antml:thinking</code> blocks from user-visible text so Anthropic-style internal monologue tags do not leak into replies. (#59550) Thanks @obviyus.</li>
|
||||
<li>Kimi Coding/tools: normalize Anthropic tool payloads into the OpenAI-compatible function shape Kimi Coding expects so tool calls stop losing required arguments. (#59440) Thanks @obviyus.</li>
|
||||
<li>Image tool/paths: resolve relative local media paths against the agent <code>workspaceDir</code> instead of <code>process.cwd()</code> so inputs like <code>inbox/receipt.png</code> pass the local-path allowlist reliably. (#57222) Thanks Priyansh Gupta.</li>
|
||||
<li>Podman/launch: remove noisy container output from <code>scripts/run-openclaw-podman.sh</code> and align the Podman install guidance with the quieter startup flow. (#59368) Thanks @sallyom.</li>
|
||||
<li>Plugins/runtime: keep LINE reply directives and browser-backed cleanup/reset flows working even when those plugins are disabled while tightening bundled plugin activation guards. (#59412) Thanks @vincentkoc.</li>
|
||||
<li>ACP/gateway reconnects: keep ACP prompts alive across transient websocket drops while still failing boundedly when reconnect recovery does not complete. (#59473) Thanks @obviyus.</li>
|
||||
<li>ACP/gateway reconnects: reject stale pre-ack ACP prompts after reconnect grace expiry so callers fail cleanly instead of hanging indefinitely when the gateway never confirms the run.</li>
|
||||
<li>Gateway/session kill: enforce HTTP operator scopes on session kill requests and gate authorization before session lookup so unauthenticated callers cannot probe session existence. (#59128) Thanks @jacobtomlinson.</li>
|
||||
<li>MS Teams/logging: format non-<code>Error</code> failures with the shared unknown-error helper so logs stop collapsing caught SDK or Axios objects into <code>[object Object]</code>. (#59321) Thanks @bradgroux.</li>
|
||||
<li>Channels/setup: ignore untrusted workspace channel plugins during setup resolution so a shadowing workspace plugin cannot override built-in channel setup/login flows unless explicitly trusted in config. (#59158) Thanks @mappel-nv.</li>
|
||||
<li>Exec/Windows: restore allowlist enforcement with quote-aware <code>argPattern</code> matching across gateway and node exec, and surface accurate dynamic pre-approved executable hints in the exec tool description. (#56285) Thanks @kpngr.</li>
|
||||
<li>Gateway: prune empty <code>node-pending-work</code> state entries after explicit acknowledgments and natural expiry so the per-node state map no longer grows indefinitely. (#58179) Thanks @gavyngong.</li>
|
||||
<li>Webhooks/secret comparison: replace ad-hoc timing-safe secret comparisons across BlueBubbles, Feishu, Mattermost, Telegram, Twilio, and Zalo webhook handlers with the shared <code>safeEqualSecret</code> helper and reject empty auth tokens in BlueBubbles. (#58432) Thanks @eleqtrizit.</li>
|
||||
<li>OpenShell/mirror: constrain <code>remoteWorkspaceDir</code> and <code>remoteAgentWorkspaceDir</code> to the managed <code>/sandbox</code> and <code>/agent</code> roots, and keep mirror sync from overwriting or removing user-added shell roots during config synchronization. (#58515) Thanks @eleqtrizit.</li>
|
||||
<li>Plugins/activation: preserve explicit, auto-enabled, and default activation provenance plus reason metadata across CLI, gateway bootstrap, and status surfaces so plugin enablement state stays accurate after auto-enable resolution. (#59641) Thanks @vincentkoc.</li>
|
||||
<li>Exec/env: block additional host environment override pivots for package roots, language runtimes, compiler include paths, and credential/config locations so request-scoped exec cannot redirect trusted toolchains or config lookups. (#59233) Thanks @drobison00.</li>
|
||||
<li>Dotenv/workspace overrides: block workspace <code>.env</code> files from overriding <code>OPENCLAW_PINNED_PYTHON</code> and <code>OPENCLAW_PINNED_WRITE_PYTHON</code> so trusted helper interpreters cannot be redirected by repo-local env injection. (#58473) Thanks @eleqtrizit.</li>
|
||||
<li>Plugins/install: accept JSON5 syntax in <code>openclaw.plugin.json</code> and bundle <code>plugin.json</code> manifests during install/validation, so third-party plugins with trailing commas, comments, or unquoted keys no longer fail to install. (#59084) Thanks @singleGanghood.</li>
|
||||
<li>Telegram/exec approvals: rewrite shared <code>/approve … allow-always</code> callback payloads to <code>/approve … always</code> before Telegram button rendering so plugin approval IDs still fit Telegram's <code>callback_data</code> limit and keep the Allow Always action visible. (#59217) Thanks @jameslcowan.</li>
|
||||
<li>Cron/exec timeouts: surface timed-out <code>exec</code> and <code>bash</code> failures in isolated cron runs even when <code>verbose: off</code>, including custom session-target cron jobs, so scheduled runs stop failing silently. (#58247) Thanks @skainguyen1412.</li>
|
||||
<li>Telegram/exec approvals: fall back to the origin session key for async approval followups and keep resume-failure status delivery sanitized so Telegram followups still land without leaking raw exec metadata. (#59351) Thanks @seonang.</li>
|
||||
<li>Node-host/exec approvals: bind <code>pnpm dlx</code> invocations through the approval planner's mutable-script path so the effective runtime command is resolved for approval instead of being left unbound. (#58374)</li>
|
||||
<li>Exec/node hosts: stop forwarding the gateway workspace cwd to remote node exec when no workdir was explicitly requested, so cross-platform node approvals fall back to the node default cwd instead of failing with <code>SYSTEM_RUN_DENIED</code>. (#58977) Thanks @Starhappysh.</li>
|
||||
<li>Exec approvals/channels: decouple initiating-surface approval availability from native delivery enablement so Telegram, Slack, and Discord still expose approvals when approvers exist and native target routing is configured separately. (#59776) Thanks @joelnishanth.</li>
|
||||
</ul>
|
||||
<h3>Changes</h3>
|
||||
<ul>
|
||||
<li>macOS/Voice Wake: add the Voice Wake option to trigger Talk Mode. (#58490) Thanks @SmoothExec.</li>
|
||||
<li>Tasks/chat: add <code>/tasks</code> as a chat-native background task board for the current session, with recent task details and agent-local fallback counts when no linked tasks are visible. Related #54226. Thanks @vincentkoc.</li>
|
||||
<li>Web search/SearXNG: add the bundled SearXNG provider plugin for <code>web_search</code> with configurable host support. (#57317) Thanks @cgdusek.</li>
|
||||
<li>Telegram/errors: add configurable <code>errorPolicy</code> and <code>errorCooldownMs</code> controls so Telegram can suppress repeated delivery errors per account, chat, and topic without muting distinct failures. (#51914) Thanks @chinar-amrutkar</li>
|
||||
<li>Gateway/webchat: make <code>chat.history</code> text truncation configurable with <code>gateway.webchat.chatHistoryMaxChars</code> and per-request <code>maxChars</code>, while preserving silent-reply filtering and existing default payload limits. (#58900)</li>
|
||||
<li>Amazon Bedrock/Guardrails: add Bedrock Guardrails support to the bundled provider. (#58588) Thanks @MikeORed.</li>
|
||||
<li>ZAI/models: add <code>glm-5.1</code> and <code>glm-5v-turbo</code> to the bundled Z.AI provider catalog. (#58793) Thanks @tomsun28</li>
|
||||
<li>Agents/default params: add <code>agents.defaults.params</code> for global default provider parameters. (#58548) Thanks @lpender.</li>
|
||||
<li>Agents/failover: cap prompt-side and assistant-side same-provider auth-profile retries for rate-limit failures before cross-provider model fallback, add the <code>auth.cooldowns.rateLimitedProfileRotations</code> knob, and document the new fallback behavior. (#58707) Thanks @Forgely3D</li>
|
||||
<li>Agents/compaction: resolve <code>agents.defaults.compaction.model</code> consistently for manual <code>/compact</code> and other context-engine compaction paths, so engine-owned compaction uses the configured override model across runtime entrypoints. (#56710) Thanks @oliviareid-svg</li>
|
||||
<li>Cron/tools allowlist: add <code>openclaw cron --tools</code> for per-job tool allowlists. (#58504) Thanks @andyk-ms.</li>
|
||||
</ul>
|
||||
<h3>Fixes</h3>
|
||||
<ul>
|
||||
<li>Chat/error replies: stop leaking raw provider/runtime failures into external chat channels, return a friendly retry message instead, and add a specific <code>/new</code> hint for Bedrock toolResult/toolUse session mismatches. (#58831) Thanks @ImLukeF.</li>
|
||||
<li>Sessions/model switching: keep <code>/model</code> changes queued behind busy runs instead of interrupting the active turn, and retarget queued followups so later work picks up the new model as soon as the current turn finishes.</li>
|
||||
<li>Web UI/OpenResponses: preserve rewritten stream snapshots in webchat and keep OpenResponses final streamed text aligned when models rewind earlier output. (#58641) Thanks @neeravmakwana</li>
|
||||
<li>Discord/inbound media: pass Discord attachment and sticker downloads through the shared idle-timeout and worker-abort path so slow or stuck inbound media fetches stop hanging message processing. (#58593) Thanks @aquaright1</li>
|
||||
<li>Telegram/retries: keep non-idempotent sends on the strict safe-send path, retry wrapped pre-connect failures, and preserve <code>429</code> / <code>retry_after</code> backoff for safe delivery retries. (#51895) Thanks @chinar-amrutkar</li>
|
||||
<li>Telegram/exec approvals: route topic-aware exec approval followups through Telegram-owned threading and approval-target parsing, so forum-topic approvals stay in the originating topic instead of falling back to the root chat. (#58783)</li>
|
||||
<li>Telegram/local Bot API: preserve media MIME types for absolute-path downloads so local audio files still trigger transcription and other MIME-based handling. (#54603) Thanks @jzakirov</li>
|
||||
<li>Channels/WhatsApp: pass inbound message timestamp to model context so the AI can see when WhatsApp messages were sent. (#58590) Thanks @Maninae</li>
|
||||
<li>QQBot/voice: lazy-load <code>silk-wasm</code> in <code>audio-convert.ts</code> so qqbot still starts when the optional voice dependency is missing, while voice encode/decode degrades gracefully instead of crashing at module load time. (#58829) Thanks @WideLee.</li>
|
||||
</ul>
|
||||
<p><a href="https://github.com/openclaw/openclaw/blob/main/CHANGELOG.md">View full changelog</a></p>
|
||||
]]></description>
|
||||
<enclosure url="https://github.com/openclaw/openclaw/releases/download/v2026.4.2/OpenClaw-2026.4.2.zip" length="25843797" type="application/octet-stream" sparkle:edSignature="bNNXr4BJEU8W7ghXOujLJTYHZL2PL/r/p4llGBw0BFL+46mJ2Bir+IK8XQaCj5zp+O5JSuh5mY+Y/Nrq6TR7Cg=="/>
|
||||
</item>
|
||||
<item>
|
||||
<title>2026.4.1</title>
|
||||
<pubDate>Wed, 01 Apr 2026 17:14:12 +0000</pubDate>
|
||||
<link>https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml</link>
|
||||
<sparkle:version>2026040190</sparkle:version>
|
||||
<sparkle:shortVersionString>2026.4.1</sparkle:shortVersionString>
|
||||
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
|
||||
<description><![CDATA[<h2>OpenClaw 2026.4.1</h2>
|
||||
<h3>Changes</h3>
|
||||
<ul>
|
||||
<li>Tasks/chat: add <code>/tasks</code> as a chat-native background task board for the current session, with recent task details and agent-local fallback counts when no linked tasks are visible. Related #54226. Thanks @vincentkoc.</li>
|
||||
<li>Web search/SearXNG: add the bundled SearXNG provider plugin for <code>web_search</code> with configurable host support. (#57317) Thanks @cgdusek.</li>
|
||||
<li>Amazon Bedrock/Guardrails: add Bedrock Guardrails support to the bundled provider. (#58588) Thanks @MikeORed.</li>
|
||||
<li>macOS/Voice Wake: add the Voice Wake option to trigger Talk Mode. (#58490) Thanks @SmoothExec.</li>
|
||||
<li>Feishu/comments: add a dedicated Drive comment-event flow with comment-thread context resolution, in-thread replies, and <code>feishu_drive</code> comment actions for document collaboration workflows. (#58497) Thanks @wittam-01.</li>
|
||||
<li>Gateway/webchat: make <code>chat.history</code> text truncation configurable with <code>gateway.webchat.chatHistoryMaxChars</code> and per-request <code>maxChars</code>, while preserving silent-reply filtering and existing default payload limits. (#58900)</li>
|
||||
<li>Agents/default params: add <code>agents.defaults.params</code> for global default provider parameters. (#58548) Thanks @lpender.</li>
|
||||
<li>Agents/failover: cap prompt-side and assistant-side same-provider auth-profile retries for rate-limit failures before cross-provider model fallback, add the <code>auth.cooldowns.rateLimitedProfileRotations</code> knob, and document the new fallback behavior. (#58707) Thanks @Forgely3D</li>
|
||||
<li>Cron/tools allowlist: add <code>openclaw cron --tools</code> for per-job tool allowlists. (#58504) Thanks @andyk-ms.</li>
|
||||
<li>Channels/session routing: move provider-specific session conversation grammar into plugin-owned session-key surfaces, preserving Telegram topic routing and Feishu scoped inheritance across bootstrap, model override, restart, and tool-policy paths.</li>
|
||||
<li>WhatsApp/reactions: add <code>reactionLevel</code> guidance for agent reactions. Thanks @mcaxtr.</li>
|
||||
<li>Telegram/errors: add configurable <code>errorPolicy</code> and <code>errorCooldownMs</code> controls so Telegram can suppress repeated delivery errors per account, chat, and topic without muting distinct failures. (#51914) Thanks @chinar-amrutkar</li>
|
||||
<li>ZAI/models: add <code>glm-5.1</code> and <code>glm-5v-turbo</code> to the bundled Z.AI provider catalog. (#58793) Thanks @tomsun28</li>
|
||||
<li>Agents/compaction: resolve <code>agents.defaults.compaction.model</code> consistently for manual <code>/compact</code> and other context-engine compaction paths, so engine-owned compaction uses the configured override model across runtime entrypoints. (#56710) Thanks @oliviareid-svg</li>
|
||||
</ul>
|
||||
<h3>Fixes</h3>
|
||||
<ul>
|
||||
<li>Chat/error replies: stop leaking raw provider/runtime failures into external chat channels, return a friendly retry message instead, and add a specific <code>/new</code> hint for Bedrock toolResult/toolUse session mismatches. (#58831) Thanks @ImLukeF.</li>
|
||||
<li>Gateway/reload: ignore startup config writes by persisted hash in the config reloader so generated auth tokens and seeded Control UI origins do not trigger a restart loop, while real <code>gateway.auth.*</code> edits still require restart. (#58678) Thanks @yelog</li>
|
||||
<li>Tasks/gateway: keep the task registry maintenance sweep from stalling the gateway event loop under synchronous SQLite pressure, so upgraded gateways stop hanging about a minute after startup. (#58670) Thanks @openperf</li>
|
||||
<li>Tasks/status: hide stale completed background tasks from <code>/status</code> and <code>session_status</code>, prefer live task context, and show recent failures only when no active work remains. (#58661) Thanks @vincentkoc</li>
|
||||
<li>Tasks/gateway: re-check the current task record before maintenance marks runs lost or prunes them, so a task heartbeat or cleanup update that lands during a sweep no longer gets overwritten by stale snapshot state.</li>
|
||||
<li>Exec/approvals: honor <code>exec-approvals.json</code> security defaults when inline or configured tool policy is unset, and keep Slack and Discord native approval handling aligned with inferred approvers and real channel enablement so remote exec stops falling into false approval timeouts and disabled states. Thanks @scoootscooob and @vincentkoc.</li>
|
||||
<li>Exec/approvals: make <code>allow-always</code> persist as durable user-approved trust instead of behaving like <code>allow-once</code>, reuse exact-command trust on shell-wrapper paths that cannot safely persist an executable allowlist entry, keep static allowlist entries from silently bypassing <code>ask:"always"</code>, and require explicit approval when Windows cannot build an allowlist execution plan instead of hard-dead-ending remote exec. Thanks @scoootscooob and @vincentkoc.</li>
|
||||
<li>Exec/cron: resolve isolated cron no-route approval dead-ends from the effective host fallback policy when trusted automation is allowed, and make <code>openclaw doctor</code> warn when <code>tools.exec</code> is broader than <code>~/.openclaw/exec-approvals.json</code> so stricter host-policy conflicts are explicit. Thanks @scoootscooob and @vincentkoc.</li>
|
||||
<li>Sessions/model switching: keep <code>/model</code> changes queued behind busy runs instead of interrupting the active turn, and retarget queued followups so later work picks up the new model as soon as the current turn finishes.</li>
|
||||
<li>Gateway/HTTP: skip failing HTTP request stages so one broken facade no longer forces every HTTP endpoint to return 500. (#58746) Thanks @yelog</li>
|
||||
<li>Gateway/nodes: stop pinning live node commands to the approved node-pair record. Node pairing remains a trust/token flow, while per-node <code>system.run</code> policy stays in that node's exec approvals config. Fixes #58824.</li>
|
||||
<li>WebChat/exec approvals: use native approval UI guidance in agent system prompts instead of telling agents to paste manual <code>/approve</code> commands in webchat sessions. Thanks @vincentkoc.</li>
|
||||
<li>Web UI/OpenResponses: preserve rewritten stream snapshots in webchat and keep OpenResponses final streamed text aligned when models rewind earlier output. (#58641) Thanks @neeravmakwana</li>
|
||||
<li>Discord/inbound media: pass Discord attachment and sticker downloads through the shared idle-timeout and worker-abort path so slow or stuck inbound media fetches stop hanging message processing. (#58593) Thanks @aquaright1</li>
|
||||
<li>Telegram/retries: keep non-idempotent sends on the strict safe-send path, retry wrapped pre-connect failures, and preserve <code>429</code> / <code>retry_after</code> backoff for safe delivery retries. (#51895) Thanks @chinar-amrutkar</li>
|
||||
<li>Telegram/exec approvals: route topic-aware exec approval followups through Telegram-owned threading and approval-target parsing, so forum-topic approvals stay in the originating topic instead of falling back to the root chat. (#58783)</li>
|
||||
<li>Telegram/local Bot API: preserve media MIME types for absolute-path downloads so local audio files still trigger transcription and other MIME-based handling. (#54603) Thanks @jzakirov</li>
|
||||
<li>Channels/WhatsApp: pass inbound message timestamp to model context so the AI can see when WhatsApp messages were sent. (#58590) Thanks @Maninae</li>
|
||||
<li>Channels/QQ Bot: keep <code>/bot-logs</code> export gated behind a truly explicit QQBot allowlist, rejecting wildcard and mixed wildcard entries while preserving the real framework command path. Thanks @vincentkoc.</li>
|
||||
<li>Channels/plugins: keep bundled channel plugins loadable from legacy <code>channels.<id></code> config even under restrictive plugin allowlists, and make <code>openclaw doctor</code> warn only on real plugin blockers instead of misleading setup guidance. (#58873) Thanks @obviyus</li>
|
||||
<li>Plugins/bundled runtimes: restore externalized bundled plugin runtime dependency staging across packed installs, Docker builds, and local runtime staging so bundled plugins keep their declared runtime deps after the 2026.3.31 externalization change. (#58782)</li>
|
||||
<li>LINE/runtime: resolve the packaged runtime contract from the built <code>dist/plugins/runtime</code> layout so LINE channels start correctly again after global npm installs on <code>2026.3.31</code>. (#58799) Thanks @vincentkoc.</li>
|
||||
<li>MiniMax/plugins: auto-enable the bundled MiniMax plugin for API-key auth/config so MiniMax image generation and other plugin-owned capabilities load without manual plugin allowlisting. (#57127) Thanks @tars90percent.</li>
|
||||
<li>Ollama/model picker: show only Ollama models after provider selection in the CLI picker. (#55290) Thanks @Luckymingxuan.</li>
|
||||
<li>CDP/profiles: prefer <code>cdpPort</code> over stale WebSocket URLs so browser automation reconnects cleanly. (#58499) Thanks @Mlightsnow.</li>
|
||||
<li>Media/paths: resolve relative <code>MEDIA</code> paths against the agent workspace so local attachment references keep working. (#58624) Thanks @aquaright1.</li>
|
||||
<li>Memory/session indexing: keep full reindexes from skipping session transcripts when sync is triggered by <code>session-start</code> or <code>watch</code>, so restart-driven reindexes preserve session memory. (#39732) Thanks @upupc</li>
|
||||
<li>Memory/QMD: prefer <code>--mask</code> over <code>--glob</code> when creating QMD collections so default memory collections keep their intended patterns and stop colliding on restart. (#58643) Thanks @GitZhangChi.</li>
|
||||
<li>Subagents/tasks: keep subagent completion and cleanup from crashing when task-registry writes fail, so a corrupt or missing task row no longer takes down the gateway during lifecycle finalization. Thanks @vincentkoc.</li>
|
||||
<li>Sandbox/browser: compare browser runtime inspection against <code>agents.defaults.sandbox.browser.image</code> so <code>openclaw sandbox list --browser</code> stops reporting healthy browser containers as image mismatches. (#58759) Thanks @sandpile.</li>
|
||||
<li>Plugins/install: forward <code>--dangerously-force-unsafe-install</code> through archive and npm-spec plugin installs so the documented override reaches the security scanner on those install paths. (#58879) Thanks @ryanlee-gemini.</li>
|
||||
<li>Auto-reply/commands: strip inbound metadata before slash command detection so wrapped <code>/model</code>, <code>/new</code>, and <code>/status</code> commands are recognized. (#58725) Thanks @Mlightsnow.</li>
|
||||
<li>Agents/Anthropic: preserve thinking blocks and signatures across replay, cache-control patching, and context pruning so compacted Anthropic sessions continue working instead of failing on later turns. (#58916) Thanks @obviyus</li>
|
||||
<li>Agents/failover: unify structured and raw provider error classification so provider-specific <code>400</code>/<code>422</code> payloads no longer get forced into generic format failures before retry, billing, or compaction logic can inspect them. (#58856) Thanks @aaron-he-zhu.</li>
|
||||
<li>Auth profiles/store: coerce misplaced SecretRef objects out of plaintext <code>key</code> and <code>token</code> fields during store load so agents without ACP runtime stop crashing on <code>.trim()</code> after upgrade. (#58923) Thanks @openperf.</li>
|
||||
<li>ACPX/runtime: repair <code>queue owner unavailable</code> session recovery by replacing dead named sessions and resuming the backend session when ACPX exposes a stable session id, so the first ACP prompt no longer inherits a dead handle. (#58669) Thanks @neeravmakwana</li>
|
||||
<li>ACPX/runtime: retry dead-session queue-owner repair without <code>--resume-session</code> when the reported ACPX session id is stale, so recovery still creates a fresh named session instead of failing session init. Thanks @obviyus.</li>
|
||||
<li>Auth/OpenAI Codex: persist plugin-refreshed OAuth credentials to <code>auth-profiles.json</code> before returning them, so rotated Codex refresh tokens survive restart and stop falling into <code>refresh_token_reused</code> loops. (#53082)</li>
|
||||
<li>Discord/gateway: hand reconnect ownership back to Carbon, keep runtime status aligned with close/reconnect state, and force-stop sockets that open without reaching READY so Discord monitors recover promptly instead of waiting on stale health timeouts. (#59019) Thanks @obviyus</li>
|
||||
</ul>
|
||||
<p><a href="https://github.com/openclaw/openclaw/blob/main/CHANGELOG.md">View full changelog</a></p>
|
||||
]]></description>
|
||||
<enclosure url="https://github.com/openclaw/openclaw/releases/download/v2026.4.1/OpenClaw-2026.4.1.zip" length="25841903" type="application/octet-stream" sparkle:edSignature="0TPiyshScmwDbgs626JU08NOUUFJmIsVFa5g0xmizfl64Fr+IoT4l/dkXarFqbZAJidtj5WN7Bff7fG8ye/7AA=="/>
|
||||
</item>
|
||||
</channel>
|
||||
</rss>
|
||||
</rss>
|
||||
@@ -65,8 +65,8 @@ android {
|
||||
applicationId = "ai.openclaw.app"
|
||||
minSdk = 31
|
||||
targetSdk = 36
|
||||
versionCode = 2026040601
|
||||
versionName = "2026.4.6"
|
||||
versionCode = 2026040901
|
||||
versionName = "2026.4.9"
|
||||
ndk {
|
||||
// Support all major ABIs — native libs are tiny (~47 KB per ABI)
|
||||
abiFilters += listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
|
||||
|
||||
13
apps/ios/CHANGELOG.md
Normal file
13
apps/ios/CHANGELOG.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# OpenClaw iOS Changelog
|
||||
|
||||
## Unreleased
|
||||
|
||||
### Added
|
||||
|
||||
### Changed
|
||||
|
||||
### Fixed
|
||||
|
||||
## 2026.4.6 - 2026-04-06
|
||||
|
||||
First App Store release of OpenClaw for iPhone. Pair with your OpenClaw Gateway to use chat, voice, sharing, and device actions from iOS.
|
||||
@@ -1,8 +1,9 @@
|
||||
// Shared iOS version defaults.
|
||||
// Generated overrides live in build/Version.xcconfig (git-ignored).
|
||||
// Source of truth: apps/ios/version.json
|
||||
// Generated by scripts/ios-sync-versioning.ts.
|
||||
|
||||
OPENCLAW_GATEWAY_VERSION = 2026.4.6
|
||||
OPENCLAW_IOS_VERSION = 2026.4.6
|
||||
OPENCLAW_MARKETING_VERSION = 2026.4.6
|
||||
OPENCLAW_BUILD_VERSION = 2026040601
|
||||
OPENCLAW_BUILD_VERSION = 1
|
||||
|
||||
#include? "../build/Version.xcconfig"
|
||||
|
||||
@@ -64,10 +64,14 @@ Release behavior:
|
||||
- Beta release uses canonical `ai.openclaw.client*` bundle IDs through a temporary generated xcconfig in `apps/ios/build/BetaRelease.xcconfig`.
|
||||
- Beta release also switches the app to `OpenClawPushTransport=relay`, `OpenClawPushDistribution=official`, and `OpenClawPushAPNsEnvironment=production`.
|
||||
- The beta flow does not modify `apps/ios/.local-signing.xcconfig` or `apps/ios/LocalSigning.xcconfig`.
|
||||
- Root `package.json.version` is the only version source for iOS.
|
||||
- A root version like `2026.4.1-beta.1` becomes:
|
||||
- `CFBundleShortVersionString = 2026.4.1`
|
||||
- `CFBundleVersion = next TestFlight build number for 2026.4.1`
|
||||
- `apps/ios/version.json` is the pinned iOS release version source.
|
||||
- `apps/ios/CHANGELOG.md` is the iOS-only changelog and release-note source.
|
||||
- The pinned iOS version must use CalVer like `2026.4.10`.
|
||||
- That pinned value becomes:
|
||||
- `CFBundleShortVersionString = 2026.4.10`
|
||||
- `CFBundleVersion = next TestFlight build number for 2026.4.10`
|
||||
- Changing the root gateway version does not change the iOS app version until you explicitly pin from the gateway.
|
||||
- See `apps/ios/VERSIONING.md` for the full workflow.
|
||||
|
||||
Required env for beta builds:
|
||||
|
||||
@@ -120,25 +124,74 @@ This should create `apps/ios/fastlane/.env` with the non-secret ASC variables wh
|
||||
export OPENCLAW_PUSH_RELAY_BASE_URL=https://relay.example.com
|
||||
```
|
||||
|
||||
4. Upload the beta:
|
||||
4. If you are starting a brand-new production release train, pin iOS to the current gateway version first:
|
||||
|
||||
```bash
|
||||
pnpm ios:version:pin -- --from-gateway
|
||||
```
|
||||
|
||||
5. Upload the beta:
|
||||
|
||||
```bash
|
||||
pnpm ios:beta
|
||||
```
|
||||
|
||||
5. Expected behavior:
|
||||
- Fastlane reads `package.json.version`
|
||||
6. Expected behavior:
|
||||
- Fastlane reads `apps/ios/version.json`
|
||||
- verifies synced iOS versioning artifacts
|
||||
- resolves the next TestFlight build number for that short version
|
||||
- generates `apps/ios/build/BetaRelease.xcconfig`
|
||||
- archives `OpenClaw`
|
||||
- uploads the IPA to TestFlight
|
||||
|
||||
6. Expected outputs after a successful run:
|
||||
7. Expected outputs after a successful run:
|
||||
- `apps/ios/build/beta/OpenClaw-<version>.ipa`
|
||||
- `apps/ios/build/beta/OpenClaw-<version>.app.dSYM.zip`
|
||||
- Fastlane log line like `Uploaded iOS beta: version=<version> short=<short> build=<build>`
|
||||
|
||||
7. If this is a fresh clone on a maintainer machine that already works elsewhere, it is OK to copy the non-secret `apps/ios/fastlane/.env` from another trusted local clone on the same Mac. The Keychain-backed private key remains machine-local and is not stored in the repo.
|
||||
8. If this is a fresh clone on a maintainer machine that already works elsewhere, it is OK to copy the non-secret `apps/ios/fastlane/.env` from another trusted local clone on the same Mac. The Keychain-backed private key remains machine-local and is not stored in the repo.
|
||||
|
||||
## iOS Versioning Workflow
|
||||
|
||||
- Pinned iOS release version: `apps/ios/version.json`
|
||||
- iOS-only changelog: `apps/ios/CHANGELOG.md`
|
||||
- Generated checked-in artifacts:
|
||||
- `apps/ios/Config/Version.xcconfig`
|
||||
- `apps/ios/fastlane/metadata/en-US/release_notes.txt`
|
||||
- Useful commands:
|
||||
|
||||
```bash
|
||||
pnpm ios:version
|
||||
pnpm ios:version:check
|
||||
pnpm ios:version:sync
|
||||
pnpm ios:version:pin -- --from-gateway
|
||||
pnpm ios:version:pin -- --version 2026.4.10
|
||||
```
|
||||
|
||||
Recommended flow:
|
||||
|
||||
### TestFlight iteration on an existing train
|
||||
|
||||
1. Keep `apps/ios/version.json` pinned to the current train version.
|
||||
2. Update `apps/ios/CHANGELOG.md`, usually under `## Unreleased` while iterating.
|
||||
3. Run `pnpm ios:version:sync` after changelog changes.
|
||||
4. Upload more TestFlight builds with `pnpm ios:beta`.
|
||||
5. Let Fastlane bump only the numeric build number.
|
||||
|
||||
### Starting the next production release train
|
||||
|
||||
1. Pin iOS to the current gateway version:
|
||||
|
||||
```bash
|
||||
pnpm ios:version:pin -- --from-gateway
|
||||
```
|
||||
|
||||
2. Update `apps/ios/CHANGELOG.md` for the new release as needed.
|
||||
3. Run `pnpm ios:version:sync`.
|
||||
4. Submit the first TestFlight build for that newly pinned version.
|
||||
5. Keep iterating on that same version until the release candidate is ready.
|
||||
|
||||
See `apps/ios/VERSIONING.md` for the detailed spec.
|
||||
|
||||
## APNs Expectations For Local/Manual Builds
|
||||
|
||||
|
||||
@@ -50,9 +50,11 @@ enum DeviceInfoHelper {
|
||||
return trimmed.isEmpty ? "unknown" : trimmed
|
||||
}
|
||||
|
||||
/// App marketing version only, e.g. "2026.2.0" or "dev".
|
||||
/// Canonical app version when present, otherwise the Apple marketing version.
|
||||
static func appVersion() -> String {
|
||||
Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "dev"
|
||||
(Bundle.main.infoDictionary?["OpenClawCanonicalVersion"] as? String)
|
||||
?? (Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String)
|
||||
?? "dev"
|
||||
}
|
||||
|
||||
/// App build string, e.g. "123" or "".
|
||||
|
||||
@@ -24,6 +24,8 @@
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(OPENCLAW_MARKETING_VERSION)</string>
|
||||
<key>OpenClawCanonicalVersion</key>
|
||||
<string>$(OPENCLAW_IOS_VERSION)</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
|
||||
150
apps/ios/VERSIONING.md
Normal file
150
apps/ios/VERSIONING.md
Normal file
@@ -0,0 +1,150 @@
|
||||
# OpenClaw iOS Versioning
|
||||
|
||||
OpenClaw iOS uses a **pinned CalVer release version** instead of reading the current gateway version automatically on every build.
|
||||
|
||||
## Goals
|
||||
|
||||
- keep TestFlight submissions on one stable app version while iterating
|
||||
- change only `CFBundleVersion` during normal TestFlight iteration
|
||||
- promote the iOS release version to the current gateway version only when a maintainer chooses to do that
|
||||
- keep Apple bundle fields valid for App Store Connect
|
||||
- generate App Store release notes from an iOS-owned changelog
|
||||
|
||||
## Version model
|
||||
|
||||
The pinned iOS release version lives in `apps/ios/version.json`.
|
||||
|
||||
Supported pinned format:
|
||||
|
||||
- `YYYY.M.D`
|
||||
|
||||
Examples:
|
||||
|
||||
- `2026.4.6`
|
||||
- `2026.4.10`
|
||||
|
||||
The root gateway version in `package.json` may still be one of:
|
||||
|
||||
- `YYYY.M.D`
|
||||
- `YYYY.M.D-beta.N`
|
||||
- `YYYY.M.D-N`
|
||||
|
||||
When you pin iOS from the gateway version, the iOS tooling strips the gateway suffix and keeps only the base CalVer.
|
||||
|
||||
Examples:
|
||||
|
||||
- gateway `2026.4.10` -> iOS `2026.4.10`
|
||||
- gateway `2026.4.10-beta.3` -> iOS `2026.4.10`
|
||||
- gateway `2026.4.10-2` -> iOS `2026.4.10`
|
||||
|
||||
## Apple bundle mapping
|
||||
|
||||
Pinned iOS version `2026.4.10` maps to:
|
||||
|
||||
- `CFBundleShortVersionString = 2026.4.10`
|
||||
- `CFBundleVersion = numeric build number only`
|
||||
|
||||
`CFBundleShortVersionString` stays fixed for a TestFlight train until you intentionally pin a newer iOS release version.
|
||||
|
||||
## Source of truth and generated files
|
||||
|
||||
### Source files
|
||||
|
||||
- `apps/ios/version.json`
|
||||
- pinned iOS release version
|
||||
- `apps/ios/CHANGELOG.md`
|
||||
- iOS-only changelog and release-note source
|
||||
- `apps/ios/VERSIONING.md`
|
||||
- workflow and constraints
|
||||
|
||||
### Generated or derived files
|
||||
|
||||
- `apps/ios/Config/Version.xcconfig`
|
||||
- checked-in defaults derived from `apps/ios/version.json`
|
||||
- `apps/ios/fastlane/metadata/en-US/release_notes.txt`
|
||||
- generated from `apps/ios/CHANGELOG.md`
|
||||
- `apps/ios/build/Version.xcconfig`
|
||||
- local gitignored build override generated per build or beta prep
|
||||
|
||||
## Tooling surfaces
|
||||
|
||||
### Version parsing and sync tooling
|
||||
|
||||
- `scripts/lib/ios-version.ts`
|
||||
- validates pinned iOS CalVer
|
||||
- normalizes gateway version -> pinned iOS CalVer
|
||||
- renders checked-in xcconfig and release notes
|
||||
- `scripts/ios-version.ts`
|
||||
- CLI for JSON, shell, or single-field version reads
|
||||
- `scripts/ios-sync-versioning.ts`
|
||||
- syncs checked-in derived files from the pinned iOS version
|
||||
- `scripts/ios-pin-version.ts`
|
||||
- explicitly pins iOS to a chosen release version or the current gateway version
|
||||
|
||||
### Build and beta flow
|
||||
|
||||
- `scripts/ios-write-version-xcconfig.sh`
|
||||
- reads the pinned iOS version
|
||||
- writes the local numeric build override file in `apps/ios/build/Version.xcconfig`
|
||||
- `scripts/ios-beta-prepare.sh`
|
||||
- prepares beta signing and bundle settings against the pinned iOS version
|
||||
- `apps/ios/fastlane/Fastfile`
|
||||
- resolves version metadata from the pinned iOS helper
|
||||
- increments TestFlight build numbers for the pinned short version
|
||||
|
||||
## Release-note resolution order
|
||||
|
||||
When generating `apps/ios/fastlane/metadata/en-US/release_notes.txt`, the tooling reads the first available changelog section in this order:
|
||||
|
||||
1. exact pinned version, for example `## 2026.4.10`
|
||||
2. `## Unreleased`
|
||||
|
||||
Recommended workflow:
|
||||
|
||||
- while iterating on a TestFlight train, keep pending notes under `## Unreleased`
|
||||
- before the production release, move or copy the final notes under `## <pinned version>` and run sync again
|
||||
|
||||
## Common commands
|
||||
|
||||
```bash
|
||||
pnpm ios:version
|
||||
pnpm ios:version:check
|
||||
pnpm ios:version:sync
|
||||
pnpm ios:version:pin -- --from-gateway
|
||||
pnpm ios:version:pin -- --version 2026.4.10
|
||||
```
|
||||
|
||||
## Normal TestFlight iteration workflow
|
||||
|
||||
1. keep `apps/ios/version.json` pinned to the current TestFlight train version
|
||||
2. update `apps/ios/CHANGELOG.md` under `## Unreleased` while iterating
|
||||
3. upload more betas with the usual flow
|
||||
4. let Fastlane increment only `CFBundleVersion`
|
||||
|
||||
This keeps the TestFlight version stable while review is in flight.
|
||||
|
||||
## New release promotion workflow
|
||||
|
||||
When you want the next production iOS release to align with the current gateway release:
|
||||
|
||||
1. pin iOS from the root gateway version:
|
||||
|
||||
```bash
|
||||
pnpm ios:version:pin -- --from-gateway
|
||||
```
|
||||
|
||||
2. review the generated changes in:
|
||||
- `apps/ios/version.json`
|
||||
- `apps/ios/Config/Version.xcconfig`
|
||||
- `apps/ios/fastlane/metadata/en-US/release_notes.txt`
|
||||
3. update `apps/ios/CHANGELOG.md` for the new release if needed
|
||||
4. run `pnpm ios:version:sync` again if the changelog changed
|
||||
5. submit the first TestFlight build for that newly pinned version
|
||||
6. keep iterating only by build number until the release candidate is ready
|
||||
7. release that reviewed TestFlight build to production
|
||||
|
||||
## Important invariant
|
||||
|
||||
Fastlane and Xcode should consume only the pinned iOS version from `apps/ios/version.json`.
|
||||
|
||||
Changing `package.json.version` alone must not change the iOS app version until a maintainer explicitly runs the pin step.
|
||||
@@ -95,35 +95,60 @@ def ios_root
|
||||
File.expand_path("..", __dir__)
|
||||
end
|
||||
|
||||
def normalize_release_version(raw_value)
|
||||
version = raw_value.to_s.strip.sub(/\Av/, "")
|
||||
UI.user_error!("Missing root package.json version.") unless env_present?(version)
|
||||
unless version.match?(/\A\d+\.\d+\.\d+(?:[.-]?beta[.-]\d+)?\z/i)
|
||||
UI.user_error!("Invalid package.json version '#{raw_value}'. Expected YYYY.M.D or YYYY.M.D-beta.N.")
|
||||
def read_ios_version_metadata
|
||||
script_path = File.join(repo_root, "scripts", "ios-version.ts")
|
||||
stdout, stderr, status = Open3.capture3(
|
||||
"node",
|
||||
"--import",
|
||||
"tsx",
|
||||
script_path,
|
||||
"--json",
|
||||
chdir: repo_root
|
||||
)
|
||||
|
||||
unless status.success?
|
||||
detail = stderr.to_s.strip
|
||||
detail = stdout.to_s.strip if detail.empty?
|
||||
UI.user_error!("Failed to read iOS version metadata: #{detail}")
|
||||
end
|
||||
|
||||
version
|
||||
end
|
||||
parsed = JSON.parse(stdout)
|
||||
version = parsed["canonicalVersion"].to_s.strip
|
||||
short_version = parsed["marketingVersion"].to_s.strip
|
||||
if !env_present?(version) || !env_present?(short_version)
|
||||
UI.user_error!("iOS version helper returned incomplete metadata.")
|
||||
end
|
||||
|
||||
def read_root_package_version
|
||||
package_json_path = File.join(repo_root, "package.json")
|
||||
UI.user_error!("Missing package.json at #{package_json_path}.") unless File.exist?(package_json_path)
|
||||
|
||||
parsed = JSON.parse(File.read(package_json_path))
|
||||
normalize_release_version(parsed["version"])
|
||||
{
|
||||
short_version: short_version,
|
||||
version: version
|
||||
}
|
||||
rescue JSON::ParserError => e
|
||||
UI.user_error!("Invalid package.json at #{package_json_path}: #{e.message}")
|
||||
UI.user_error!("Invalid JSON from iOS version helper: #{e.message}")
|
||||
end
|
||||
|
||||
def short_release_version(version)
|
||||
normalize_release_version(version).sub(/([.-]?beta[.-]\d+)\z/i, "")
|
||||
def sync_ios_versioning!
|
||||
script_path = File.join(repo_root, "scripts", "ios-sync-versioning.ts")
|
||||
stdout, stderr, status = Open3.capture3(
|
||||
"node",
|
||||
"--import",
|
||||
"tsx",
|
||||
script_path,
|
||||
"--check",
|
||||
chdir: repo_root
|
||||
)
|
||||
return if status.success?
|
||||
|
||||
detail = stderr.to_s.strip
|
||||
detail = stdout.to_s.strip if detail.empty?
|
||||
UI.user_error!("iOS versioning artifacts are stale. Run `pnpm ios:version:sync`.\n#{detail}")
|
||||
end
|
||||
|
||||
def shell_join(parts)
|
||||
Shellwords.join(parts.compact)
|
||||
end
|
||||
|
||||
def resolve_beta_build_number(api_key:, version:)
|
||||
def resolve_beta_build_number(api_key:, short_version:)
|
||||
explicit = ENV["IOS_BETA_BUILD_NUMBER"]
|
||||
if env_present?(explicit)
|
||||
UI.user_error!("Invalid IOS_BETA_BUILD_NUMBER '#{explicit}'. Expected digits only.") unless explicit.match?(/\A\d+\z/)
|
||||
@@ -131,7 +156,6 @@ def resolve_beta_build_number(api_key:, version:)
|
||||
return explicit
|
||||
end
|
||||
|
||||
short_version = short_release_version(version)
|
||||
latest_build = latest_testflight_build_number(
|
||||
api_key: api_key,
|
||||
app_identifier: BETA_APP_IDENTIFIER,
|
||||
@@ -244,15 +268,18 @@ platform :ios do
|
||||
require_api_key = options[:require_api_key] == true
|
||||
needs_api_key = require_api_key || beta_build_number_needs_asc_auth?
|
||||
api_key = needs_api_key ? asc_api_key : nil
|
||||
version = read_root_package_version
|
||||
build_number = resolve_beta_build_number(api_key: api_key, version: version)
|
||||
sync_ios_versioning!
|
||||
version_metadata = read_ios_version_metadata
|
||||
version = version_metadata[:version]
|
||||
short_version = version_metadata[:short_version]
|
||||
build_number = resolve_beta_build_number(api_key: api_key, short_version: short_version)
|
||||
beta_xcconfig = prepare_beta_release!(version: version, build_number: build_number)
|
||||
|
||||
{
|
||||
api_key: api_key,
|
||||
beta_xcconfig: beta_xcconfig,
|
||||
build_number: build_number,
|
||||
short_version: short_release_version(version),
|
||||
short_version: short_version,
|
||||
version: version
|
||||
}
|
||||
end
|
||||
@@ -286,6 +313,7 @@ platform :ios do
|
||||
|
||||
desc "Upload App Store metadata (and optionally screenshots)"
|
||||
lane :metadata do
|
||||
sync_ios_versioning!
|
||||
api_key = asc_api_key
|
||||
clear_empty_env_var("APP_STORE_CONNECT_API_KEY_PATH")
|
||||
app_identifier = ENV["ASC_APP_IDENTIFIER"]
|
||||
|
||||
@@ -109,13 +109,19 @@ cd apps/ios
|
||||
fastlane ios auth_check
|
||||
```
|
||||
|
||||
4. Set the official/TestFlight relay URL before release:
|
||||
4. If you are starting a brand-new production release train, pin iOS to the current gateway version:
|
||||
|
||||
```bash
|
||||
pnpm ios:version:pin -- --from-gateway
|
||||
```
|
||||
|
||||
5. Set the official/TestFlight relay URL before release:
|
||||
|
||||
```bash
|
||||
export OPENCLAW_PUSH_RELAY_BASE_URL=https://relay.example.com
|
||||
```
|
||||
|
||||
5. Upload:
|
||||
6. Upload:
|
||||
|
||||
```bash
|
||||
pnpm ios:beta
|
||||
@@ -129,9 +135,15 @@ Quick verification after upload:
|
||||
|
||||
Versioning rules:
|
||||
|
||||
- Root `package.json.version` is the single source of truth for iOS
|
||||
- Use `YYYY.M.D` for stable versions and `YYYY.M.D-beta.N` for beta versions
|
||||
- Fastlane stamps `CFBundleShortVersionString` to `YYYY.M.D`
|
||||
- `apps/ios/version.json` is the pinned iOS release version source
|
||||
- `apps/ios/CHANGELOG.md` is the iOS-only changelog and release-note source
|
||||
- Supported pinned iOS versions use CalVer: `YYYY.M.D`
|
||||
- `pnpm ios:version:pin -- --from-gateway` promotes the current root gateway version into the pinned iOS release version
|
||||
- Fastlane uses the pinned iOS version only; changing `package.json.version` alone does not change the iOS app version
|
||||
- Fastlane sets `CFBundleShortVersionString` to the pinned iOS version, for example `2026.4.10`
|
||||
- Fastlane resolves `CFBundleVersion` as the next integer TestFlight build number for that short version
|
||||
- Run `pnpm ios:version:sync` after changing `apps/ios/version.json` or `apps/ios/CHANGELOG.md`
|
||||
- `pnpm ios:version:check` validates that checked-in iOS version artifacts are in sync
|
||||
- The beta flow regenerates `apps/ios/OpenClaw.xcodeproj` from `apps/ios/project.yml` before archiving
|
||||
- Local beta signing uses a temporary generated xcconfig and leaves local development signing overrides untouched
|
||||
- See `apps/ios/VERSIONING.md` for the detailed workflow
|
||||
|
||||
@@ -36,6 +36,9 @@ Or set `APP_STORE_CONNECT_API_KEY_PATH`.
|
||||
## Notes
|
||||
|
||||
- Locale files live under `metadata/en-US/`.
|
||||
- `release_notes.txt` is generated from `apps/ios/CHANGELOG.md`; after changelog updates, run `pnpm ios:version:sync`.
|
||||
- Release notes resolve from `## <pinned iOS version>` first, then fall back to `## Unreleased` while a TestFlight train is still in progress.
|
||||
- When starting a new production release train, pin the iOS version first with `pnpm ios:version:pin -- --from-gateway`.
|
||||
- `privacy_url.txt` is set to `https://openclaw.ai/privacy`.
|
||||
- If app lookup fails in `deliver`, set one of:
|
||||
- `ASC_APP_IDENTIFIER` (bundle ID)
|
||||
|
||||
@@ -119,6 +119,7 @@ targets:
|
||||
CFBundleURLSchemes:
|
||||
- openclaw
|
||||
CFBundleShortVersionString: "$(OPENCLAW_MARKETING_VERSION)"
|
||||
OpenClawCanonicalVersion: "$(OPENCLAW_IOS_VERSION)"
|
||||
CFBundleVersion: "$(OPENCLAW_BUILD_VERSION)"
|
||||
UILaunchScreen: {}
|
||||
UIApplicationSceneManifest:
|
||||
|
||||
3
apps/ios/version.json
Normal file
3
apps/ios/version.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"version": "2026.4.6"
|
||||
}
|
||||
@@ -6,168 +6,180 @@ import Foundation
|
||||
|
||||
enum HostEnvSecurityPolicy {
|
||||
static let blockedKeys: Set<String> = [
|
||||
"NODE_OPTIONS",
|
||||
"NODE_PATH",
|
||||
"PYTHONHOME",
|
||||
"PYTHONPATH",
|
||||
"PERL5LIB",
|
||||
"PERL5OPT",
|
||||
"RUBYLIB",
|
||||
"RUBYOPT",
|
||||
"_JAVA_OPTIONS",
|
||||
"ANT_OPTS",
|
||||
"BASH_ENV",
|
||||
"ENV",
|
||||
"BROWSER",
|
||||
"GIT_EDITOR",
|
||||
"GIT_EXTERNAL_DIFF",
|
||||
"GIT_EXEC_PATH",
|
||||
"GIT_SEQUENCE_EDITOR",
|
||||
"GIT_TEMPLATE_DIR",
|
||||
"GIT_SSL_NO_VERIFY",
|
||||
"GIT_SSL_CAINFO",
|
||||
"GIT_SSL_CAPATH",
|
||||
"CC",
|
||||
"CXX",
|
||||
"CARGO_BUILD_RUSTC",
|
||||
"CARGO_BUILD_RUSTC_WRAPPER",
|
||||
"RUSTC_WRAPPER",
|
||||
"CC",
|
||||
"CMAKE_C_COMPILER",
|
||||
"CMAKE_CXX_COMPILER",
|
||||
"SHELL",
|
||||
"SHELLOPTS",
|
||||
"PS4",
|
||||
"CXX",
|
||||
"DOTNET_ADDITIONAL_DEPS",
|
||||
"DOTNET_STARTUP_HOOKS",
|
||||
"ENV",
|
||||
"GCONV_PATH",
|
||||
"GIT_ALTERNATE_OBJECT_DIRECTORIES",
|
||||
"GIT_COMMON_DIR",
|
||||
"GIT_DIR",
|
||||
"GIT_EDITOR",
|
||||
"GIT_EXEC_PATH",
|
||||
"GIT_EXTERNAL_DIFF",
|
||||
"GIT_INDEX_FILE",
|
||||
"GIT_NAMESPACE",
|
||||
"GIT_OBJECT_DIRECTORY",
|
||||
"GIT_SEQUENCE_EDITOR",
|
||||
"GIT_SSL_CAINFO",
|
||||
"GIT_SSL_CAPATH",
|
||||
"GIT_SSL_NO_VERIFY",
|
||||
"GIT_TEMPLATE_DIR",
|
||||
"GIT_WORK_TREE",
|
||||
"GLIBC_TUNABLES",
|
||||
"GRADLE_OPTS",
|
||||
"HGRCPATH",
|
||||
"IFS",
|
||||
"SSLKEYLOGFILE",
|
||||
"JAVA_OPTS",
|
||||
"JAVA_TOOL_OPTIONS",
|
||||
"_JAVA_OPTIONS",
|
||||
"JDK_JAVA_OPTIONS",
|
||||
"PYTHONBREAKPOINT",
|
||||
"DOTNET_STARTUP_HOOKS",
|
||||
"DOTNET_ADDITIONAL_DEPS",
|
||||
"GLIBC_TUNABLES",
|
||||
"MAVEN_OPTS",
|
||||
"MAKEFLAGS",
|
||||
"MAVEN_OPTS",
|
||||
"MFLAGS",
|
||||
"NODE_OPTIONS",
|
||||
"NODE_PATH",
|
||||
"PERL5LIB",
|
||||
"PERL5OPT",
|
||||
"PS4",
|
||||
"PYTHONBREAKPOINT",
|
||||
"PYTHONHOME",
|
||||
"PYTHONPATH",
|
||||
"RUBYLIB",
|
||||
"RUBYOPT",
|
||||
"RUSTC_WRAPPER",
|
||||
"SBT_OPTS",
|
||||
"GRADLE_OPTS",
|
||||
"ANT_OPTS",
|
||||
"HGRCPATH"
|
||||
"SHELL",
|
||||
"SHELLOPTS",
|
||||
"SSLKEYLOGFILE"
|
||||
]
|
||||
|
||||
static let blockedOverrideKeys: Set<String> = [
|
||||
"HOME",
|
||||
"GRADLE_USER_HOME",
|
||||
"ZDOTDIR",
|
||||
"GIT_SSH_COMMAND",
|
||||
"GIT_SSH",
|
||||
"GIT_PROXY_COMMAND",
|
||||
"GIT_ASKPASS",
|
||||
"GIT_SSL_NO_VERIFY",
|
||||
"GIT_SSL_CAINFO",
|
||||
"GIT_SSL_CAPATH",
|
||||
"SSH_ASKPASS",
|
||||
"LESSOPEN",
|
||||
"LESSCLOSE",
|
||||
"PAGER",
|
||||
"MANPAGER",
|
||||
"GIT_PAGER",
|
||||
"EDITOR",
|
||||
"VISUAL",
|
||||
"FCEDIT",
|
||||
"SUDO_EDITOR",
|
||||
"PROMPT_COMMAND",
|
||||
"HISTFILE",
|
||||
"PERL5DB",
|
||||
"PERL5DBCMD",
|
||||
"OPENSSL_CONF",
|
||||
"OPENSSL_ENGINES",
|
||||
"PYTHONSTARTUP",
|
||||
"WGETRC",
|
||||
"CURL_HOME",
|
||||
"CLASSPATH",
|
||||
"ALL_PROXY",
|
||||
"AWS_CONFIG_FILE",
|
||||
"AWS_SHARED_CREDENTIALS_FILE",
|
||||
"AWS_WEB_IDENTITY_TOKEN_FILE",
|
||||
"AZURE_AUTH_LOCATION",
|
||||
"BUN_CONFIG_REGISTRY",
|
||||
"BUNDLE_GEMFILE",
|
||||
"C_INCLUDE_PATH",
|
||||
"CARGO_BUILD_RUSTC_WRAPPER",
|
||||
"CARGO_HOME",
|
||||
"CGO_CFLAGS",
|
||||
"CGO_LDFLAGS",
|
||||
"GOFLAGS",
|
||||
"MAKEFLAGS",
|
||||
"MFLAGS",
|
||||
"CLASSPATH",
|
||||
"COMPOSER_HOME",
|
||||
"CORECLR_PROFILER_PATH",
|
||||
"PHPRC",
|
||||
"PHP_INI_SCAN_DIR",
|
||||
"DENO_DIR",
|
||||
"BUN_CONFIG_REGISTRY",
|
||||
"YARN_RC_FILENAME",
|
||||
"HTTP_PROXY",
|
||||
"HTTPS_PROXY",
|
||||
"ALL_PROXY",
|
||||
"NO_PROXY",
|
||||
"NODE_TLS_REJECT_UNAUTHORIZED",
|
||||
"NODE_EXTRA_CA_CERTS",
|
||||
"SSL_CERT_FILE",
|
||||
"SSL_CERT_DIR",
|
||||
"REQUESTS_CA_BUNDLE",
|
||||
"CPATH",
|
||||
"CPLUS_INCLUDE_PATH",
|
||||
"CURL_CA_BUNDLE",
|
||||
"CURL_HOME",
|
||||
"DENO_DIR",
|
||||
"DOCKER_CERT_PATH",
|
||||
"DOCKER_CONTEXT",
|
||||
"DOCKER_HOST",
|
||||
"DOCKER_TLS_VERIFY",
|
||||
"DOCKER_CERT_PATH",
|
||||
"EDITOR",
|
||||
"FCEDIT",
|
||||
"GEM_HOME",
|
||||
"GEM_PATH",
|
||||
"GIT_ALTERNATE_OBJECT_DIRECTORIES",
|
||||
"GIT_ASKPASS",
|
||||
"GIT_COMMON_DIR",
|
||||
"GIT_DIR",
|
||||
"GIT_INDEX_FILE",
|
||||
"GIT_NAMESPACE",
|
||||
"GIT_OBJECT_DIRECTORY",
|
||||
"GIT_PAGER",
|
||||
"GIT_PROXY_COMMAND",
|
||||
"GIT_SSH",
|
||||
"GIT_SSH_COMMAND",
|
||||
"GIT_SSL_CAINFO",
|
||||
"GIT_SSL_CAPATH",
|
||||
"GIT_SSL_NO_VERIFY",
|
||||
"GIT_WORK_TREE",
|
||||
"GOENV",
|
||||
"GOFLAGS",
|
||||
"GONOPROXY",
|
||||
"GONOSUMCHECK",
|
||||
"GONOSUMDB",
|
||||
"GOOGLE_APPLICATION_CREDENTIALS",
|
||||
"GOPATH",
|
||||
"GOPRIVATE",
|
||||
"GOPROXY",
|
||||
"GRADLE_USER_HOME",
|
||||
"HELM_HOME",
|
||||
"HGRCPATH",
|
||||
"HISTFILE",
|
||||
"HOME",
|
||||
"HTTP_PROXY",
|
||||
"HTTPS_PROXY",
|
||||
"KUBECONFIG",
|
||||
"LESSCLOSE",
|
||||
"LESSOPEN",
|
||||
"LIBRARY_PATH",
|
||||
"LUA_CPATH",
|
||||
"LUA_PATH",
|
||||
"MAKEFLAGS",
|
||||
"MANPAGER",
|
||||
"MFLAGS",
|
||||
"NO_PROXY",
|
||||
"NODE_EXTRA_CA_CERTS",
|
||||
"NODE_TLS_REJECT_UNAUTHORIZED",
|
||||
"OBJC_INCLUDE_PATH",
|
||||
"OPENSSL_CONF",
|
||||
"OPENSSL_ENGINES",
|
||||
"PAGER",
|
||||
"PERL5DB",
|
||||
"PERL5DBCMD",
|
||||
"PHP_INI_SCAN_DIR",
|
||||
"PHPRC",
|
||||
"PIP_CONFIG_FILE",
|
||||
"PIP_EXTRA_INDEX_URL",
|
||||
"PIP_FIND_LINKS",
|
||||
"PIP_INDEX_URL",
|
||||
"PIP_PYPI_URL",
|
||||
"PIP_EXTRA_INDEX_URL",
|
||||
"PIP_CONFIG_FILE",
|
||||
"PIP_FIND_LINKS",
|
||||
"PIP_TRUSTED_HOST",
|
||||
"PROMPT_COMMAND",
|
||||
"PYTHONSTARTUP",
|
||||
"PYTHONUSERBASE",
|
||||
"REQUESTS_CA_BUNDLE",
|
||||
"RUSTC_WRAPPER",
|
||||
"RUSTFLAGS",
|
||||
"SSH_ASKPASS",
|
||||
"SSL_CERT_DIR",
|
||||
"SSL_CERT_FILE",
|
||||
"SUDO_EDITOR",
|
||||
"UV_DEFAULT_INDEX",
|
||||
"UV_EXTRA_INDEX_URL",
|
||||
"UV_INDEX",
|
||||
"UV_INDEX_URL",
|
||||
"UV_PYTHON",
|
||||
"UV_EXTRA_INDEX_URL",
|
||||
"UV_DEFAULT_INDEX",
|
||||
"DOCKER_HOST",
|
||||
"DOCKER_TLS_VERIFY",
|
||||
"DOCKER_CERT_PATH",
|
||||
"DOCKER_CONTEXT",
|
||||
"LIBRARY_PATH",
|
||||
"CPATH",
|
||||
"C_INCLUDE_PATH",
|
||||
"CPLUS_INCLUDE_PATH",
|
||||
"OBJC_INCLUDE_PATH",
|
||||
"NODE_EXTRA_CA_CERTS",
|
||||
"SSL_CERT_FILE",
|
||||
"SSL_CERT_DIR",
|
||||
"REQUESTS_CA_BUNDLE",
|
||||
"CURL_CA_BUNDLE",
|
||||
"GOPROXY",
|
||||
"GONOSUMCHECK",
|
||||
"GONOSUMDB",
|
||||
"GONOPROXY",
|
||||
"GOPRIVATE",
|
||||
"GOENV",
|
||||
"GOPATH",
|
||||
"HGRCPATH",
|
||||
"PYTHONUSERBASE",
|
||||
"RUSTC_WRAPPER",
|
||||
"RUSTFLAGS",
|
||||
"CARGO_HOME",
|
||||
"VIRTUAL_ENV",
|
||||
"LUA_PATH",
|
||||
"LUA_CPATH",
|
||||
"GEM_HOME",
|
||||
"GEM_PATH",
|
||||
"BUNDLE_GEMFILE",
|
||||
"COMPOSER_HOME",
|
||||
"CARGO_BUILD_RUSTC_WRAPPER",
|
||||
"VISUAL",
|
||||
"WGETRC",
|
||||
"XDG_CONFIG_HOME",
|
||||
"AWS_CONFIG_FILE"
|
||||
"YARN_RC_FILENAME",
|
||||
"ZDOTDIR"
|
||||
]
|
||||
|
||||
static let blockedOverridePrefixes: [String] = [
|
||||
"CARGO_REGISTRIES_",
|
||||
"GIT_CONFIG_",
|
||||
"NPM_CONFIG_",
|
||||
"CARGO_REGISTRIES_"
|
||||
"NPM_CONFIG_"
|
||||
]
|
||||
|
||||
static let blockedPrefixes: [String] = [
|
||||
"BASH_FUNC_",
|
||||
"DYLD_",
|
||||
"LD_",
|
||||
"BASH_FUNC_"
|
||||
"LD_"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -15,9 +15,9 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>2026.4.6</string>
|
||||
<string>2026.4.9</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>2026040601</string>
|
||||
<string>2026040901</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>OpenClaw</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
f7b342080a730da84d1ac84a888e9506d24ee7ce7ec6ec6c0cc4f1897fabcde3 config-baseline.json
|
||||
c3dd9fb8a0059dba411c4d88a6b84ca28af1e0b1925c669058ef9f38c6d2718b config-baseline.core.json
|
||||
d22f4414b79ee03d896e58d875c80523bcc12303cbacb1700261e6ec73945187 config-baseline.channel.json
|
||||
d42cee3dea4668bdb7daf6ff5e6f87f326fdef56a8c3716d73079b92cab6e7b2 config-baseline.plugin.json
|
||||
6092701439f9f56624f508eb2b240cb48375264c2667a99cb7e7823cb0ef18d1 config-baseline.json
|
||||
065f474b340fc22b19358cb298131037cbb2a3411ef0b6f765072bbaafedf751 config-baseline.core.json
|
||||
7f42b22b46c487d64aaac46001ba9d9096cf7bf0b1c263a54d39946303ff5018 config-baseline.channel.json
|
||||
483d4f3c1d516719870ad6f2aba6779b9950f85471ee77b9994a077a7574a892 config-baseline.plugin.json
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
2efa99907731355b31a1b95a6baa9cf5bf8d25c67931837857c9bb9dd39fad95 plugin-sdk-api-baseline.json
|
||||
6c99467113b5d6a015cbd424f2eb5c7e21a6c665b3e8d0372e0e09a2218ef13e plugin-sdk-api-baseline.jsonl
|
||||
39ef29d01ee9fceeb63c11f8454040f84d34852a773556d43600e71d7d923f64 plugin-sdk-api-baseline.json
|
||||
d7d7bf77272ea41c4bfe992b980cf1b8088911df7aca522de3b716d28cb568bf plugin-sdk-api-baseline.jsonl
|
||||
|
||||
@@ -183,6 +183,14 @@
|
||||
"source": "Doctor",
|
||||
"target": "Doctor"
|
||||
},
|
||||
{
|
||||
"source": "Memory Wiki",
|
||||
"target": "Memory Wiki"
|
||||
},
|
||||
{
|
||||
"source": "wiki",
|
||||
"target": "wiki"
|
||||
},
|
||||
{
|
||||
"source": "Polls",
|
||||
"target": "投票"
|
||||
|
||||
@@ -1003,6 +1003,8 @@ Core examples:
|
||||
- moderation: `timeout`, `kick`, `ban`
|
||||
- presence: `setPresence`
|
||||
|
||||
The `event-create` action accepts an optional `image` parameter (URL or local file path) to set the scheduled event cover image.
|
||||
|
||||
Action gates live under `channels.discord.actions.*`.
|
||||
|
||||
Default gate behavior:
|
||||
|
||||
@@ -463,9 +463,11 @@ Notes:
|
||||
- `block`: append chunked preview updates.
|
||||
- `progress`: show progress status text while generating, then send final text.
|
||||
|
||||
`channels.slack.nativeStreaming` controls Slack native text streaming when `streaming` is `partial` (default: `true`).
|
||||
`channels.slack.streaming.nativeTransport` controls Slack native text streaming when `channels.slack.streaming.mode` is `partial` (default: `true`).
|
||||
|
||||
- A reply thread must be available for native text streaming to appear. Thread selection still follows `replyToMode`. Without one, the normal draft preview is used.
|
||||
- A reply thread must be available for native text streaming and Slack assistant thread status to appear. Thread selection still follows `replyToMode`.
|
||||
- Channel and group-chat roots can still use the normal draft preview when native streaming is unavailable.
|
||||
- Top-level Slack DMs stay off-thread by default, so they do not show the thread-style preview; use thread replies or `typingReaction` if you want visible progress there.
|
||||
- Media and non-text payloads fall back to normal delivery.
|
||||
- If streaming fails mid-reply, OpenClaw falls back to normal delivery for remaining payloads.
|
||||
|
||||
@@ -475,8 +477,10 @@ Use draft preview instead of Slack native text streaming:
|
||||
{
|
||||
channels: {
|
||||
slack: {
|
||||
streaming: "partial",
|
||||
nativeStreaming: false,
|
||||
streaming: {
|
||||
mode: "partial",
|
||||
nativeTransport: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -484,8 +488,9 @@ Use draft preview instead of Slack native text streaming:
|
||||
|
||||
Legacy keys:
|
||||
|
||||
- `channels.slack.streamMode` (`replace | status_final | append`) is auto-migrated to `channels.slack.streaming`.
|
||||
- boolean `channels.slack.streaming` is auto-migrated to `channels.slack.nativeStreaming`.
|
||||
- `channels.slack.streamMode` (`replace | status_final | append`) is auto-migrated to `channels.slack.streaming.mode`.
|
||||
- boolean `channels.slack.streaming` is auto-migrated to `channels.slack.streaming.mode` and `channels.slack.streaming.nativeTransport`.
|
||||
- legacy `channels.slack.nativeStreaming` is auto-migrated to `channels.slack.streaming.nativeTransport`.
|
||||
|
||||
## Typing reaction fallback
|
||||
|
||||
@@ -687,7 +692,7 @@ Primary reference:
|
||||
- compatibility toggle: `dangerouslyAllowNameMatching` (break-glass; keep off unless needed)
|
||||
- channel access: `groupPolicy`, `channels.*`, `channels.*.users`, `channels.*.requireMention`
|
||||
- threading/history: `replyToMode`, `replyToModeByChatType`, `thread.*`, `historyLimit`, `dmHistoryLimit`, `dms.*.historyLimit`
|
||||
- delivery: `textChunkLimit`, `chunkMode`, `mediaMaxMb`, `streaming`, `nativeStreaming`
|
||||
- delivery: `textChunkLimit`, `chunkMode`, `mediaMaxMb`, `streaming`, `streaming.nativeTransport`
|
||||
- ops/features: `configWrites`, `commands.native`, `slashCommand.*`, `actions.*`, `userToken`, `userTokenReadOnly`
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
@@ -37,6 +37,7 @@ This page describes the current CLI behavior. If commands change, update this do
|
||||
- [`models`](/cli/models)
|
||||
- [`infer`](/cli/infer)
|
||||
- [`memory`](/cli/memory)
|
||||
- [`wiki`](/cli/wiki)
|
||||
- [`directory`](/cli/directory)
|
||||
- [`nodes`](/cli/nodes)
|
||||
- [`devices`](/cli/devices)
|
||||
@@ -162,6 +163,19 @@ openclaw [--dev] [--profile <name>] <command>
|
||||
status
|
||||
index
|
||||
search
|
||||
wiki
|
||||
status
|
||||
doctor
|
||||
init
|
||||
ingest
|
||||
compile
|
||||
lint
|
||||
search
|
||||
get
|
||||
apply
|
||||
bridge import
|
||||
unsafe-local import
|
||||
obsidian status|search|open|command|daily
|
||||
message
|
||||
send
|
||||
broadcast
|
||||
|
||||
@@ -15,6 +15,8 @@ Provided by the active memory plugin (default: `memory-core`; set `plugins.slots
|
||||
Related:
|
||||
|
||||
- Memory concept: [Memory](/concepts/memory)
|
||||
- Memory wiki: [Memory Wiki](/plugins/memory-wiki)
|
||||
- Wiki CLI: [wiki](/cli/wiki)
|
||||
- Plugins: [Plugins](/tools/plugin)
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -115,7 +115,7 @@ Interactive onboarding behavior with reference mode:
|
||||
|
||||
Non-interactive Z.AI endpoint choices:
|
||||
|
||||
Note: `--auth-choice zai-api-key` now auto-detects the best Z.AI endpoint for your key (prefers the general API with `zai/glm-5`).
|
||||
Note: `--auth-choice zai-api-key` now auto-detects the best Z.AI endpoint for your key (prefers the general API with `zai/glm-5.1`).
|
||||
If you specifically want the GLM Coding Plan endpoints, pick `zai-coding-global` or `zai-coding-cn`.
|
||||
|
||||
```bash
|
||||
|
||||
214
docs/cli/wiki.md
Normal file
214
docs/cli/wiki.md
Normal file
@@ -0,0 +1,214 @@
|
||||
---
|
||||
summary: "CLI reference for `openclaw wiki` (memory-wiki vault status, search, compile, lint, apply, bridge, and Obsidian helpers)"
|
||||
read_when:
|
||||
- You want to use the memory-wiki CLI
|
||||
- You are documenting or changing `openclaw wiki`
|
||||
title: "wiki"
|
||||
---
|
||||
|
||||
# `openclaw wiki`
|
||||
|
||||
Inspect and maintain the `memory-wiki` vault.
|
||||
|
||||
Provided by the bundled `memory-wiki` plugin.
|
||||
|
||||
Related:
|
||||
|
||||
- [Memory Wiki plugin](/plugins/memory-wiki)
|
||||
- [Memory Overview](/concepts/memory)
|
||||
- [CLI: memory](/cli/memory)
|
||||
|
||||
## What it is for
|
||||
|
||||
Use `openclaw wiki` when you want a compiled knowledge vault with:
|
||||
|
||||
- wiki-native search and page reads
|
||||
- provenance-rich syntheses
|
||||
- contradiction and freshness reports
|
||||
- bridge imports from the active memory plugin
|
||||
- optional Obsidian CLI helpers
|
||||
|
||||
## Common commands
|
||||
|
||||
```bash
|
||||
openclaw wiki status
|
||||
openclaw wiki doctor
|
||||
openclaw wiki init
|
||||
openclaw wiki ingest ./notes/alpha.md
|
||||
openclaw wiki compile
|
||||
openclaw wiki lint
|
||||
openclaw wiki search "alpha"
|
||||
openclaw wiki get entity.alpha --from 1 --lines 80
|
||||
|
||||
openclaw wiki apply synthesis "Alpha Summary" \
|
||||
--body "Short synthesis body" \
|
||||
--source-id source.alpha
|
||||
|
||||
openclaw wiki apply metadata entity.alpha \
|
||||
--source-id source.alpha \
|
||||
--status review \
|
||||
--question "Still active?"
|
||||
|
||||
openclaw wiki bridge import
|
||||
openclaw wiki unsafe-local import
|
||||
|
||||
openclaw wiki obsidian status
|
||||
openclaw wiki obsidian search "alpha"
|
||||
openclaw wiki obsidian open syntheses/alpha-summary.md
|
||||
openclaw wiki obsidian command workspace:quick-switcher
|
||||
openclaw wiki obsidian daily
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
### `wiki status`
|
||||
|
||||
Inspect current vault mode, health, and Obsidian CLI availability.
|
||||
|
||||
Use this first when you are unsure whether the vault is initialized, bridge mode
|
||||
is healthy, or Obsidian integration is available.
|
||||
|
||||
### `wiki doctor`
|
||||
|
||||
Run wiki health checks and surface configuration or vault problems.
|
||||
|
||||
Typical issues include:
|
||||
|
||||
- bridge mode enabled without public memory artifacts
|
||||
- invalid or missing vault layout
|
||||
- missing external Obsidian CLI when Obsidian mode is expected
|
||||
|
||||
### `wiki init`
|
||||
|
||||
Create the wiki vault layout and starter pages.
|
||||
|
||||
This initializes the root structure, including top-level indexes and cache
|
||||
directories.
|
||||
|
||||
### `wiki ingest <path-or-url>`
|
||||
|
||||
Import content into the wiki source layer.
|
||||
|
||||
Notes:
|
||||
|
||||
- URL ingest is controlled by `ingest.allowUrlIngest`
|
||||
- imported source pages keep provenance in frontmatter
|
||||
- auto-compile can run after ingest when enabled
|
||||
|
||||
### `wiki compile`
|
||||
|
||||
Rebuild indexes, related blocks, dashboards, and compiled digests.
|
||||
|
||||
This writes stable machine-facing artifacts under:
|
||||
|
||||
- `.openclaw-wiki/cache/agent-digest.json`
|
||||
- `.openclaw-wiki/cache/claims.jsonl`
|
||||
|
||||
If `render.createDashboards` is enabled, compile also refreshes report pages.
|
||||
|
||||
### `wiki lint`
|
||||
|
||||
Lint the vault and report:
|
||||
|
||||
- structural issues
|
||||
- provenance gaps
|
||||
- contradictions
|
||||
- open questions
|
||||
- low-confidence pages/claims
|
||||
- stale pages/claims
|
||||
|
||||
Run this after meaningful wiki updates.
|
||||
|
||||
### `wiki search <query>`
|
||||
|
||||
Search wiki content.
|
||||
|
||||
Behavior depends on config:
|
||||
|
||||
- `search.backend`: `shared` or `local`
|
||||
- `search.corpus`: `wiki`, `memory`, or `all`
|
||||
|
||||
Use `wiki search` when you want wiki-specific ranking or provenance details.
|
||||
For one broad shared recall pass, prefer `openclaw memory search` when the
|
||||
active memory plugin exposes shared search.
|
||||
|
||||
### `wiki get <lookup>`
|
||||
|
||||
Read a wiki page by id or relative path.
|
||||
|
||||
Examples:
|
||||
|
||||
```bash
|
||||
openclaw wiki get entity.alpha
|
||||
openclaw wiki get syntheses/alpha-summary.md --from 1 --lines 80
|
||||
```
|
||||
|
||||
### `wiki apply`
|
||||
|
||||
Apply narrow mutations without freeform page surgery.
|
||||
|
||||
Supported flows include:
|
||||
|
||||
- create/update a synthesis page
|
||||
- update page metadata
|
||||
- attach source ids
|
||||
- add questions
|
||||
- add contradictions
|
||||
- update confidence/status
|
||||
- write structured claims
|
||||
|
||||
This command exists so the wiki can evolve safely without manually editing
|
||||
managed blocks.
|
||||
|
||||
### `wiki bridge import`
|
||||
|
||||
Import public memory artifacts from the active memory plugin into bridge-backed
|
||||
source pages.
|
||||
|
||||
Use this in `bridge` mode when you want the latest exported memory artifacts
|
||||
pulled into the wiki vault.
|
||||
|
||||
### `wiki unsafe-local import`
|
||||
|
||||
Import from explicitly configured local paths in `unsafe-local` mode.
|
||||
|
||||
This is intentionally experimental and same-machine only.
|
||||
|
||||
### `wiki obsidian ...`
|
||||
|
||||
Obsidian helper commands for vaults running in Obsidian-friendly mode.
|
||||
|
||||
Subcommands:
|
||||
|
||||
- `status`
|
||||
- `search`
|
||||
- `open`
|
||||
- `command`
|
||||
- `daily`
|
||||
|
||||
These require the official `obsidian` CLI on `PATH` when
|
||||
`obsidian.useOfficialCli` is enabled.
|
||||
|
||||
## Practical usage guidance
|
||||
|
||||
- Use `wiki search` + `wiki get` when provenance and page identity matter.
|
||||
- Use `wiki apply` instead of hand-editing managed generated sections.
|
||||
- Use `wiki lint` before trusting contradictory or low-confidence content.
|
||||
- Use `wiki compile` after bulk imports or source changes when you want fresh
|
||||
dashboards and compiled digests immediately.
|
||||
- Use `wiki bridge import` when bridge mode depends on newly exported memory
|
||||
artifacts.
|
||||
|
||||
## Configuration tie-ins
|
||||
|
||||
`openclaw wiki` behavior is shaped by:
|
||||
|
||||
- `plugins.entries.memory-wiki.config.vaultMode`
|
||||
- `plugins.entries.memory-wiki.config.search.backend`
|
||||
- `plugins.entries.memory-wiki.config.search.corpus`
|
||||
- `plugins.entries.memory-wiki.config.bridge.*`
|
||||
- `plugins.entries.memory-wiki.config.obsidian.*`
|
||||
- `plugins.entries.memory-wiki.config.render.*`
|
||||
- `plugins.entries.memory-wiki.config.context.includeCompiledDigestPrompt`
|
||||
|
||||
See [Memory Wiki plugin](/plugins/memory-wiki) for the full config model.
|
||||
@@ -1,504 +0,0 @@
|
||||
---
|
||||
title: "Active Memory"
|
||||
summary: "A plugin-owned blocking memory subagent that injects relevant memory into interactive chat sessions"
|
||||
read_when:
|
||||
- You want to understand what active memory is for
|
||||
- You want to turn active memory on for a conversational agent
|
||||
- You want to tune active memory behavior without enabling it everywhere
|
||||
---
|
||||
|
||||
# Active Memory
|
||||
|
||||
Active memory is an optional plugin-owned blocking memory subagent that runs
|
||||
before the main reply for eligible conversational sessions.
|
||||
|
||||
It exists because most memory systems are capable but reactive. They rely on
|
||||
the main agent to decide when to search memory, or on the user to say things
|
||||
like "remember this" or "search memory." By then, the moment where memory would
|
||||
have made the reply feel natural has already passed.
|
||||
|
||||
Active memory gives the system one bounded chance to surface relevant memory
|
||||
before the main reply is generated.
|
||||
|
||||
## Paste This Into Your Agent
|
||||
|
||||
Paste this into your agent if you want it to enable Active Memory with a
|
||||
self-contained, safe-default setup:
|
||||
|
||||
```json5
|
||||
{
|
||||
plugins: {
|
||||
entries: {
|
||||
"active-memory": {
|
||||
enabled: true,
|
||||
config: {
|
||||
agents: ["main"],
|
||||
allowedChatTypes: ["direct"],
|
||||
modelFallbackPolicy: "default-remote",
|
||||
queryMode: "recent",
|
||||
timeoutMs: 15000,
|
||||
maxSummaryChars: 220,
|
||||
persistTranscripts: false,
|
||||
logging: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
This turns the plugin on for the `main` agent, keeps it limited to direct-message
|
||||
style sessions by default, lets it inherit the current session model first, and
|
||||
still allows the built-in remote fallback if no explicit or inherited model is
|
||||
available.
|
||||
|
||||
After that, restart the gateway:
|
||||
|
||||
```bash
|
||||
node scripts/run-node.mjs gateway --profile dev
|
||||
```
|
||||
|
||||
To inspect it live in a conversation:
|
||||
|
||||
```text
|
||||
/verbose on
|
||||
```
|
||||
|
||||
## Turn active memory on
|
||||
|
||||
The safest setup is:
|
||||
|
||||
1. enable the plugin
|
||||
2. target one conversational agent
|
||||
3. keep logging on only while tuning
|
||||
|
||||
Start with this in `openclaw.json`:
|
||||
|
||||
```json5
|
||||
{
|
||||
plugins: {
|
||||
entries: {
|
||||
"active-memory": {
|
||||
enabled: true,
|
||||
config: {
|
||||
agents: ["main"],
|
||||
allowedChatTypes: ["direct"],
|
||||
modelFallbackPolicy: "default-remote",
|
||||
queryMode: "recent",
|
||||
timeoutMs: 15000,
|
||||
maxSummaryChars: 220,
|
||||
persistTranscripts: false,
|
||||
logging: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Then restart the gateway:
|
||||
|
||||
```bash
|
||||
node scripts/run-node.mjs gateway --profile dev
|
||||
```
|
||||
|
||||
What this means:
|
||||
|
||||
- `plugins.entries.active-memory.enabled: true` turns the plugin on
|
||||
- `config.agents: ["main"]` opts only the `main` agent into active memory
|
||||
- `config.allowedChatTypes: ["direct"]` keeps active memory on for direct-message style sessions only by default
|
||||
- if `config.model` is unset, active memory inherits the current session model first
|
||||
- `config.modelFallbackPolicy: "default-remote"` keeps the built-in remote fallback as the default when no explicit or inherited model is available
|
||||
- active memory still runs only on eligible interactive persistent chat sessions
|
||||
|
||||
## How to see it
|
||||
|
||||
Active memory injects hidden system context for the model. It does not expose
|
||||
raw `<active_memory_plugin>...</active_memory_plugin>` tags to the client.
|
||||
|
||||
If you want to see what active memory is doing in a live session, turn verbose
|
||||
mode on for that session:
|
||||
|
||||
```text
|
||||
/verbose on
|
||||
```
|
||||
|
||||
With verbose enabled, OpenClaw can show:
|
||||
|
||||
- an active memory status line such as `Active Memory: ok 842ms recent 34 chars`
|
||||
- a readable debug summary such as `Active Memory Debug: Lemon pepper wings with blue cheese.`
|
||||
|
||||
Those lines are derived from the same active memory pass that feeds the hidden
|
||||
system context, but they are formatted for humans instead of exposing raw prompt
|
||||
markup.
|
||||
|
||||
By default, the blocking memory subagent transcript is temporary and deleted
|
||||
after the run completes.
|
||||
|
||||
Example flow:
|
||||
|
||||
```text
|
||||
/verbose on
|
||||
what wings should i order?
|
||||
```
|
||||
|
||||
Expected visible reply shape:
|
||||
|
||||
```text
|
||||
...normal assistant reply...
|
||||
|
||||
🧩 Active Memory: ok 842ms recent 34 chars
|
||||
🔎 Active Memory Debug: Lemon pepper wings with blue cheese.
|
||||
```
|
||||
|
||||
## When it runs
|
||||
|
||||
Active memory uses two gates:
|
||||
|
||||
1. **Config opt-in**
|
||||
The plugin must be enabled, and the current agent id must appear in
|
||||
`plugins.entries.active-memory.config.agents`.
|
||||
2. **Strict runtime eligibility**
|
||||
Even when enabled and targeted, active memory only runs for eligible
|
||||
interactive persistent chat sessions.
|
||||
|
||||
The actual rule is:
|
||||
|
||||
```text
|
||||
plugin enabled
|
||||
+
|
||||
agent id targeted
|
||||
+
|
||||
allowed chat type
|
||||
+
|
||||
eligible interactive persistent chat session
|
||||
=
|
||||
active memory runs
|
||||
```
|
||||
|
||||
If any of those fail, active memory does not run.
|
||||
|
||||
## Session types
|
||||
|
||||
`config.allowedChatTypes` controls which kinds of conversations may run Active
|
||||
Memory at all.
|
||||
|
||||
The default is:
|
||||
|
||||
```json5
|
||||
allowedChatTypes: ["direct"]
|
||||
```
|
||||
|
||||
That means Active Memory runs by default in direct-message style sessions, but
|
||||
not in group or channel sessions unless you opt them in explicitly.
|
||||
|
||||
Examples:
|
||||
|
||||
```json5
|
||||
allowedChatTypes: ["direct"]
|
||||
```
|
||||
|
||||
```json5
|
||||
allowedChatTypes: ["direct", "group"]
|
||||
```
|
||||
|
||||
```json5
|
||||
allowedChatTypes: ["direct", "group", "channel"]
|
||||
```
|
||||
|
||||
## Where it runs
|
||||
|
||||
Active memory is a conversational enrichment feature, not a platform-wide
|
||||
inference feature.
|
||||
|
||||
| Surface | Runs active memory? |
|
||||
| ------------------------------------------------------------------- | ------------------------------------------------------- |
|
||||
| Control UI / web chat persistent sessions | Yes, if the plugin is enabled and the agent is targeted |
|
||||
| Other interactive channel sessions on the same persistent chat path | Yes, if the plugin is enabled and the agent is targeted |
|
||||
| Headless one-shot runs | No |
|
||||
| Heartbeat/background runs | No |
|
||||
| Generic internal `agent-command` paths | No |
|
||||
| Subagent/internal helper execution | No |
|
||||
|
||||
## Why use it
|
||||
|
||||
Use active memory when:
|
||||
|
||||
- the session is persistent and user-facing
|
||||
- the agent has meaningful long-term memory to search
|
||||
- continuity and personalization matter more than raw prompt determinism
|
||||
|
||||
It works especially well for:
|
||||
|
||||
- stable preferences
|
||||
- recurring habits
|
||||
- long-term user context that should surface naturally
|
||||
|
||||
It is a poor fit for:
|
||||
|
||||
- automation
|
||||
- internal workers
|
||||
- one-shot API tasks
|
||||
- places where hidden personalization would be surprising
|
||||
|
||||
## How it works
|
||||
|
||||
The runtime shape is:
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
U["User Message"] --> Q["Build Memory Query"]
|
||||
Q --> R["Active Memory Blocking Memory Subagent"]
|
||||
R -->|NONE or empty| M["Main Reply"]
|
||||
R -->|relevant summary| I["Append Hidden active_memory_plugin System Context"]
|
||||
I --> M["Main Reply"]
|
||||
```
|
||||
|
||||
The blocking memory subagent can use only:
|
||||
|
||||
- `memory_search`
|
||||
- `memory_get`
|
||||
|
||||
If the connection is weak, it should return `NONE`.
|
||||
|
||||
## Query modes
|
||||
|
||||
`config.queryMode` controls how much conversation the blocking memory subagent sees.
|
||||
|
||||
## Model fallback policy
|
||||
|
||||
If `config.model` is unset, Active Memory tries to resolve a model in this order:
|
||||
|
||||
```text
|
||||
explicit plugin model
|
||||
-> current session model
|
||||
-> agent primary model
|
||||
-> optional built-in remote fallback
|
||||
```
|
||||
|
||||
`config.modelFallbackPolicy` controls the last step.
|
||||
|
||||
Default:
|
||||
|
||||
```json5
|
||||
modelFallbackPolicy: "default-remote"
|
||||
```
|
||||
|
||||
Other option:
|
||||
|
||||
```json5
|
||||
modelFallbackPolicy: "resolved-only"
|
||||
```
|
||||
|
||||
Use `resolved-only` if you want Active Memory to skip recall instead of falling
|
||||
back to the built-in remote default when no explicit or inherited model is
|
||||
available.
|
||||
|
||||
### `message`
|
||||
|
||||
Only the latest user message is sent.
|
||||
|
||||
```text
|
||||
Latest user message only
|
||||
```
|
||||
|
||||
Use this when:
|
||||
|
||||
- you want the fastest behavior
|
||||
- you want the strongest bias toward stable preference recall
|
||||
- follow-up turns do not need conversational context
|
||||
|
||||
Recommended timeout:
|
||||
|
||||
- start around `3000` to `5000` ms
|
||||
|
||||
### `recent`
|
||||
|
||||
The latest user message plus a small recent conversational tail is sent.
|
||||
|
||||
```text
|
||||
Recent conversation tail:
|
||||
user: ...
|
||||
assistant: ...
|
||||
user: ...
|
||||
|
||||
Latest user message:
|
||||
...
|
||||
```
|
||||
|
||||
Use this when:
|
||||
|
||||
- you want a better balance of speed and conversational grounding
|
||||
- follow-up questions often depend on the last few turns
|
||||
|
||||
Recommended timeout:
|
||||
|
||||
- start around `15000` ms
|
||||
|
||||
### `full`
|
||||
|
||||
The full conversation is sent to the blocking memory subagent.
|
||||
|
||||
```text
|
||||
Full conversation context:
|
||||
user: ...
|
||||
assistant: ...
|
||||
user: ...
|
||||
...
|
||||
```
|
||||
|
||||
Use this when:
|
||||
|
||||
- the strongest recall quality matters more than latency
|
||||
- the conversation contains important setup far back in the thread
|
||||
|
||||
Recommended timeout:
|
||||
|
||||
- increase it substantially compared with `message` or `recent`
|
||||
- start around `15000` ms or higher depending on thread size
|
||||
|
||||
In general, timeout should increase with context size:
|
||||
|
||||
```text
|
||||
message < recent < full
|
||||
```
|
||||
|
||||
## Transcript persistence
|
||||
|
||||
Active memory blocking memory subagent runs create a real `session.jsonl`
|
||||
transcript during the blocking memory subagent call.
|
||||
|
||||
By default, that transcript is temporary:
|
||||
|
||||
- it is written to a temp directory
|
||||
- it is used only for the blocking memory subagent run
|
||||
- it is deleted immediately after the run finishes
|
||||
|
||||
If you want to keep those blocking memory subagent transcripts on disk for debugging or
|
||||
inspection, turn persistence on explicitly:
|
||||
|
||||
```json5
|
||||
{
|
||||
plugins: {
|
||||
entries: {
|
||||
"active-memory": {
|
||||
enabled: true,
|
||||
config: {
|
||||
agents: ["main"],
|
||||
persistTranscripts: true,
|
||||
transcriptDir: "active-memory",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
When enabled, active memory stores transcripts in a separate directory under the
|
||||
target agent's sessions folder, not in the main user conversation transcript
|
||||
path.
|
||||
|
||||
The default layout is conceptually:
|
||||
|
||||
```text
|
||||
agents/<agent>/sessions/active-memory/<blocking-memory-subagent-session-id>.jsonl
|
||||
```
|
||||
|
||||
You can change the relative subdirectory with `config.transcriptDir`.
|
||||
|
||||
Use this carefully:
|
||||
|
||||
- blocking memory subagent transcripts can accumulate quickly on busy sessions
|
||||
- `full` query mode can duplicate a lot of conversation context
|
||||
- these transcripts contain hidden prompt context and recalled memories
|
||||
|
||||
## Configuration
|
||||
|
||||
All active memory configuration lives under:
|
||||
|
||||
```text
|
||||
plugins.entries.active-memory
|
||||
```
|
||||
|
||||
The most important fields are:
|
||||
|
||||
| Key | Type | Meaning |
|
||||
| --------------------------- | --------------------------------- | ----------------------------------------------------------------------------------------------------- |
|
||||
| `enabled` | `boolean` | Enables the plugin itself |
|
||||
| `config.agents` | `string[]` | Agent ids that may use active memory |
|
||||
| `config.model` | `string` | Optional blocking memory subagent model ref; when unset, active memory uses the current session model |
|
||||
| `config.queryMode` | `"message" \| "recent" \| "full"` | Controls how much conversation the blocking memory subagent sees |
|
||||
| `config.timeoutMs` | `number` | Hard timeout for the blocking memory subagent |
|
||||
| `config.maxSummaryChars` | `number` | Maximum total characters allowed in the active-memory summary |
|
||||
| `config.logging` | `boolean` | Emits active memory logs while tuning |
|
||||
| `config.persistTranscripts` | `boolean` | Keeps blocking memory subagent transcripts on disk instead of deleting temp files |
|
||||
| `config.transcriptDir` | `string` | Relative blocking memory subagent transcript directory under the agent sessions folder |
|
||||
|
||||
Useful tuning fields:
|
||||
|
||||
| Key | Type | Meaning |
|
||||
| ----------------------------- | -------- | ------------------------------------------------------------- |
|
||||
| `config.maxSummaryChars` | `number` | Maximum total characters allowed in the active-memory summary |
|
||||
| `config.recentUserTurns` | `number` | Prior user turns to include when `queryMode` is `recent` |
|
||||
| `config.recentAssistantTurns` | `number` | Prior assistant turns to include when `queryMode` is `recent` |
|
||||
| `config.recentUserChars` | `number` | Max chars per recent user turn |
|
||||
| `config.recentAssistantChars` | `number` | Max chars per recent assistant turn |
|
||||
| `config.cacheTtlMs` | `number` | Cache reuse for repeated identical queries |
|
||||
|
||||
## Recommended setup
|
||||
|
||||
Start with `recent`.
|
||||
|
||||
```json5
|
||||
{
|
||||
plugins: {
|
||||
entries: {
|
||||
"active-memory": {
|
||||
enabled: true,
|
||||
config: {
|
||||
agents: ["main"],
|
||||
queryMode: "recent",
|
||||
timeoutMs: 15000,
|
||||
maxSummaryChars: 220,
|
||||
logging: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
If you want to inspect live behavior while tuning, use `/verbose on` in the
|
||||
session instead of looking for a separate active-memory debug command.
|
||||
|
||||
Then move to:
|
||||
|
||||
- `message` if you want lower latency
|
||||
- `full` if you decide extra context is worth the slower blocking memory subagent
|
||||
|
||||
## Debugging
|
||||
|
||||
If active memory is not showing up where you expect:
|
||||
|
||||
1. Confirm the plugin is enabled under `plugins.entries.active-memory.enabled`.
|
||||
2. Confirm the current agent id is listed in `config.agents`.
|
||||
3. Confirm you are testing through an interactive persistent chat session.
|
||||
4. Turn on `config.logging: true` and watch the gateway logs.
|
||||
5. Verify memory search itself works with `openclaw memory status --deep`.
|
||||
|
||||
If memory hits are noisy, tighten:
|
||||
|
||||
- `maxSummaryChars`
|
||||
|
||||
If active memory is too slow:
|
||||
|
||||
- lower `queryMode`
|
||||
- lower `timeoutMs`
|
||||
- reduce recent turn counts
|
||||
- reduce per-turn char caps
|
||||
|
||||
## Related pages
|
||||
|
||||
- [Memory Search](/concepts/memory-search)
|
||||
- [Memory configuration reference](/reference/memory-config)
|
||||
- [Plugin SDK setup](/plugins/sdk-setup)
|
||||
@@ -42,7 +42,7 @@ These phases are internal implementation details, not separate user-configured
|
||||
Light phase ingests recent daily memory signals and recall traces, dedupes them,
|
||||
and stages candidate lines.
|
||||
|
||||
- Reads from short-term recall state and recent daily memory files.
|
||||
- Reads from short-term recall state, recent daily memory files, and redacted session transcripts when available.
|
||||
- Writes a managed `## Light Sleep` block when storage includes inline output.
|
||||
- Records reinforcement signals for later deep ranking.
|
||||
- Never writes to `MEMORY.md`.
|
||||
@@ -66,6 +66,13 @@ REM phase extracts patterns and reflective signals.
|
||||
- Records REM reinforcement signals used by deep ranking.
|
||||
- Never writes to `MEMORY.md`.
|
||||
|
||||
## Session transcript ingestion
|
||||
|
||||
Dreaming can ingest redacted session transcripts into the dreaming corpus. When
|
||||
transcripts are available, they are fed into the light phase alongside daily
|
||||
memory signals and recall traces. Personal and sensitive content is redacted
|
||||
before ingestion.
|
||||
|
||||
## Dream Diary
|
||||
|
||||
Dreaming also keeps a narrative **Dream Diary** in `DREAMS.md`.
|
||||
|
||||
@@ -138,6 +138,5 @@ earlier conversations. This is opt-in via
|
||||
|
||||
## Further reading
|
||||
|
||||
- [Active Memory](/concepts/active-memory) -- sidecar memory for interactive chat sessions
|
||||
- [Memory](/concepts/memory) -- file layout, backends, tools
|
||||
- [Memory configuration reference](/reference/memory-config) -- all config knobs
|
||||
|
||||
@@ -40,6 +40,26 @@ The agent has two tools for working with memory:
|
||||
|
||||
Both tools are provided by the active memory plugin (default: `memory-core`).
|
||||
|
||||
## Memory Wiki companion plugin
|
||||
|
||||
If you want durable memory to behave more like a maintained knowledge base than
|
||||
just raw notes, use the bundled `memory-wiki` plugin.
|
||||
|
||||
`memory-wiki` compiles durable knowledge into a wiki vault with:
|
||||
|
||||
- deterministic page structure
|
||||
- structured claims and evidence
|
||||
- contradiction and freshness tracking
|
||||
- generated dashboards
|
||||
- compiled digests for agent/runtime consumers
|
||||
- wiki-native tools like `wiki_search`, `wiki_get`, `wiki_apply`, and `wiki_lint`
|
||||
|
||||
It does not replace the active memory plugin. The active memory plugin still
|
||||
owns recall, promotion, and dreaming. `memory-wiki` adds a provenance-rich
|
||||
knowledge layer beside it.
|
||||
|
||||
See [Memory Wiki](/plugins/memory-wiki).
|
||||
|
||||
## Memory search
|
||||
|
||||
When an embedding provider is configured, `memory_search` uses **hybrid
|
||||
@@ -73,6 +93,15 @@ multi-agent awareness. Plugin install.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
## Knowledge wiki layer
|
||||
|
||||
<CardGroup cols={1}>
|
||||
<Card title="Memory Wiki" icon="book" href="/plugins/memory-wiki">
|
||||
Compiles durable memory into a provenance-rich wiki vault with claims,
|
||||
dashboards, bridge mode, and Obsidian-friendly workflows.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
## Automatic memory flush
|
||||
|
||||
Before [compaction](/concepts/compaction) summarizes your conversation, OpenClaw
|
||||
@@ -117,6 +146,7 @@ openclaw memory index --force # Rebuild the index
|
||||
- [Builtin Memory Engine](/concepts/memory-builtin) -- default SQLite backend
|
||||
- [QMD Memory Engine](/concepts/memory-qmd) -- advanced local-first sidecar
|
||||
- [Honcho Memory](/concepts/memory-honcho) -- AI-native cross-session memory
|
||||
- [Memory Wiki](/plugins/memory-wiki) -- compiled knowledge vault and wiki-native tools
|
||||
- [Memory Search](/concepts/memory-search) -- search pipeline, providers, and
|
||||
tuning
|
||||
- [Dreaming (experimental)](/concepts/dreaming) -- background promotion
|
||||
|
||||
@@ -371,7 +371,7 @@ OpenClaw ships with the pi‑ai catalog. These providers require **no**
|
||||
|
||||
- Provider: `zai`
|
||||
- Auth: `ZAI_API_KEY`
|
||||
- Example model: `zai/glm-5`
|
||||
- Example model: `zai/glm-5.1`
|
||||
- CLI: `openclaw onboard --auth-choice zai-api-key`
|
||||
- Aliases: `z.ai/*` and `z-ai/*` normalize to `zai/*`
|
||||
- `zai-api-key` auto-detects the matching Z.AI endpoint; `zai-coding-global`, `zai-coding-cn`, `zai-global`, and `zai-cn` force a specific surface
|
||||
|
||||
@@ -56,8 +56,8 @@ asset hash changes.
|
||||
|
||||
Seed assets live in `qa/`:
|
||||
|
||||
- `qa/QA_KICKOFF_TASK.md`
|
||||
- `qa/seed-scenarios.json`
|
||||
- `qa/scenarios/index.md`
|
||||
- `qa/scenarios/*.md`
|
||||
|
||||
These are intentionally in git so the QA plan is visible to both humans and the
|
||||
agent. The baseline list should stay broad enough to cover:
|
||||
|
||||
@@ -126,13 +126,14 @@ Modes:
|
||||
|
||||
Slack-only:
|
||||
|
||||
- `channels.slack.nativeStreaming` toggles Slack native streaming API calls when `streaming=partial` (default: `true`).
|
||||
- `channels.slack.streaming.nativeTransport` toggles Slack native streaming API calls when `channels.slack.streaming.mode="partial"` (default: `true`).
|
||||
- Slack native streaming and Slack assistant thread status require a reply thread target; top-level DMs do not show that thread-style preview.
|
||||
|
||||
Legacy key migration:
|
||||
|
||||
- Telegram: `streamMode` + boolean `streaming` auto-migrate to `streaming` enum.
|
||||
- Discord: `streamMode` + boolean `streaming` auto-migrate to `streaming` enum.
|
||||
- Slack: `streamMode` auto-migrates to `streaming` enum; boolean `streaming` auto-migrates to `nativeStreaming`.
|
||||
- Slack: `streamMode` auto-migrates to `streaming.mode`; boolean `streaming` auto-migrates to `streaming.mode` plus `streaming.nativeTransport`; legacy `nativeStreaming` auto-migrates to `streaming.nativeTransport`.
|
||||
|
||||
### Runtime behavior
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ The prompt is intentionally compact and uses fixed sections:
|
||||
- **Sandbox** (when enabled): indicates sandboxed runtime, sandbox paths, and whether elevated exec is available.
|
||||
- **Current Date & Time**: user-local time, timezone, and time format.
|
||||
- **Reply Tags**: optional reply tag syntax for supported providers.
|
||||
- **Heartbeats**: heartbeat prompt and ack behavior.
|
||||
- **Heartbeats**: heartbeat prompt and ack behavior, when heartbeats are enabled for the default agent.
|
||||
- **Runtime**: host, OS, node, model, repo root (when detected), thinking level (one line).
|
||||
- **Reasoning**: current visibility level + /reasoning toggle hint.
|
||||
|
||||
@@ -103,10 +103,12 @@ Bootstrap files are trimmed and appended under **Project Context** so the model
|
||||
- `BOOTSTRAP.md` (only on brand-new workspaces)
|
||||
- `MEMORY.md` when present, otherwise `memory.md` as a lowercase fallback
|
||||
|
||||
All of these files are **injected into the context window** on every turn, which
|
||||
means they consume tokens. Keep them concise — especially `MEMORY.md`, which can
|
||||
grow over time and lead to unexpectedly high context usage and more frequent
|
||||
compaction.
|
||||
All of these files are **injected into the context window** on every turn unless
|
||||
a file-specific gate applies. `HEARTBEAT.md` is omitted on normal runs when
|
||||
heartbeats are disabled for the default agent or
|
||||
`agents.defaults.heartbeat.includeSystemPromptSection` is false. Keep injected
|
||||
files concise — especially `MEMORY.md`, which can grow over time and lead to
|
||||
unexpectedly high context usage and more frequent compaction.
|
||||
|
||||
> **Note:** `memory/*.md` daily files are **not** injected automatically. They
|
||||
> are accessed on demand via the `memory_search` and `memory_get` tools, so they
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: "Configuration Reference"
|
||||
summary: "Complete reference for every OpenClaw config key, defaults, and channel settings"
|
||||
summary: "Gateway config reference for core OpenClaw keys, defaults, and links to dedicated subsystem references"
|
||||
read_when:
|
||||
- You need exact field-level config semantics or defaults
|
||||
- You are validating channel, model, gateway, or tool config blocks
|
||||
@@ -8,7 +8,21 @@ read_when:
|
||||
|
||||
# Configuration Reference
|
||||
|
||||
Every field available in `~/.openclaw/openclaw.json`. For a task-oriented overview, see [Configuration](/gateway/configuration).
|
||||
Core config reference for `~/.openclaw/openclaw.json`. For a task-oriented overview, see [Configuration](/gateway/configuration).
|
||||
|
||||
This page covers the main OpenClaw config surfaces and links out when a subsystem has its own deeper reference. It does **not** try to inline every channel/plugin-owned command catalog or every deep memory/QMD knob on one page.
|
||||
|
||||
Code truth:
|
||||
|
||||
- `openclaw config schema` prints the live JSON Schema used for validation and Control UI, with bundled/plugin/channel metadata merged in when available
|
||||
- `config.schema.lookup` returns one path-scoped schema node for drill-down tooling
|
||||
- `pnpm config:docs:check` / `pnpm config:docs:gen` validate the config-doc baseline hash against the current schema surface
|
||||
|
||||
Dedicated deep references:
|
||||
|
||||
- [Memory configuration reference](/reference/memory-config) for `agents.defaults.memorySearch.*`, `memory.qmd.*`, `memory.citations`, and dreaming config under `plugins.entries.memory-core.config.dreaming`
|
||||
- [Slash Commands](/tools/slash-commands) for the current built-in + bundled command catalog
|
||||
- owning channel/plugin pages for channel-specific command surfaces
|
||||
|
||||
Config format is **JSON5** (comments + trailing commas allowed). All fields are optional — OpenClaw uses safe defaults when omitted.
|
||||
|
||||
@@ -426,8 +440,10 @@ WhatsApp runs through the gateway's web channel (Baileys Web). It starts automat
|
||||
typingReaction: "hourglass_flowing_sand",
|
||||
textChunkLimit: 4000,
|
||||
chunkMode: "length",
|
||||
streaming: "partial", // off | partial | block | progress (preview mode)
|
||||
nativeStreaming: true, // use Slack native streaming API when streaming=partial
|
||||
streaming: {
|
||||
mode: "partial", // off | partial | block | progress
|
||||
nativeTransport: true, // use Slack native streaming API when mode=partial
|
||||
},
|
||||
mediaMaxMb: 20,
|
||||
execApprovals: {
|
||||
enabled: "auto", // true | false | "auto"
|
||||
@@ -452,13 +468,14 @@ WhatsApp runs through the gateway's web channel (Baileys Web). It starts automat
|
||||
resolve the secret value.
|
||||
- `configWrites: false` blocks Slack-initiated config writes.
|
||||
- Optional `channels.slack.defaultAccount` overrides default account selection when it matches a configured account id.
|
||||
- `channels.slack.streaming` is the canonical stream mode key. Legacy `streamMode` and boolean `streaming` values are auto-migrated.
|
||||
- `channels.slack.streaming.mode` is the canonical Slack stream mode key. `channels.slack.streaming.nativeTransport` controls Slack's native streaming transport. Legacy `streamMode`, boolean `streaming`, and `nativeStreaming` values are auto-migrated.
|
||||
- Use `user:<id>` (DM) or `channel:<id>` for delivery targets.
|
||||
|
||||
**Reaction notification modes:** `off`, `own` (default), `all`, `allowlist` (from `reactionAllowlist`).
|
||||
|
||||
**Thread session isolation:** `thread.historyScope` is per-thread (default) or shared across channel. `thread.inheritParent` copies parent channel transcript to new threads.
|
||||
|
||||
- Slack native streaming plus the Slack assistant-style "is typing..." thread status require a reply thread target. Top-level DMs stay off-thread by default, so they use `typingReaction` or normal delivery instead of the thread-style preview.
|
||||
- `typingReaction` adds a temporary reaction to the inbound Slack message while a reply is running, then removes it on completion. Use a Slack emoji shortcode such as `"hourglass_flowing_sand"`.
|
||||
- `channels.slack.execApprovals`: Slack-native exec approval delivery and approver authorization. Same schema as Discord: `enabled` (`true`/`false`/`"auto"`), `approvers` (Slack user IDs), `agentFilter`, `sessionFilter`, and `target` (`"dm"`, `"channel"`, or `"both"`).
|
||||
|
||||
@@ -814,12 +831,18 @@ Include your own number in `allowFrom` to enable self-chat mode (ignores native
|
||||
{
|
||||
commands: {
|
||||
native: "auto", // register native commands when supported
|
||||
nativeSkills: "auto", // register native skill commands when supported
|
||||
text: true, // parse /commands in chat messages
|
||||
bash: false, // allow ! (alias: /bash)
|
||||
bashForegroundMs: 2000,
|
||||
config: false, // allow /config
|
||||
mcp: false, // allow /mcp
|
||||
plugins: false, // allow /plugins
|
||||
debug: false, // allow /debug
|
||||
restart: false, // allow /restart + gateway restart tool
|
||||
restart: true, // allow /restart + gateway restart tool
|
||||
ownerAllowFrom: ["discord:123456789012345678"],
|
||||
ownerDisplay: "raw", // raw | hash
|
||||
ownerDisplaySecret: "${OWNER_ID_HASH_SECRET}",
|
||||
allowFrom: {
|
||||
"*": ["user1"],
|
||||
discord: ["user:123"],
|
||||
@@ -831,16 +854,32 @@ Include your own number in `allowFrom` to enable self-chat mode (ignores native
|
||||
|
||||
<Accordion title="Command details">
|
||||
|
||||
- This block configures command surfaces. For the current built-in + bundled command catalog, see [Slash Commands](/tools/slash-commands).
|
||||
- This page is a **config-key reference**, not the full command catalog. Channel/plugin-owned commands such as QQ Bot `/bot-ping` `/bot-help` `/bot-logs`, LINE `/card`, device-pair `/pair`, memory `/dreaming`, phone-control `/phone`, and Talk `/voice` are documented in their channel/plugin pages plus [Slash Commands](/tools/slash-commands).
|
||||
- Text commands must be **standalone** messages with leading `/`.
|
||||
- `native: "auto"` turns on native commands for Discord/Telegram, leaves Slack off.
|
||||
- `nativeSkills: "auto"` turns on native skill commands for Discord/Telegram, leaves Slack off.
|
||||
- Override per channel: `channels.discord.commands.native` (bool or `"auto"`). `false` clears previously registered commands.
|
||||
- Override native skill registration per channel with `channels.<provider>.commands.nativeSkills`.
|
||||
- `channels.telegram.customCommands` adds extra Telegram bot menu entries.
|
||||
- `bash: true` enables `! <cmd>` for host shell. Requires `tools.elevated.enabled` and sender in `tools.elevated.allowFrom.<channel>`.
|
||||
- `config: true` enables `/config` (reads/writes `openclaw.json`). For gateway `chat.send` clients, persistent `/config set|unset` writes also require `operator.admin`; read-only `/config show` stays available to normal write-scoped operator clients.
|
||||
- `mcp: true` enables `/mcp` for OpenClaw-managed MCP server config under `mcp.servers`.
|
||||
- `plugins: true` enables `/plugins` for plugin discovery, install, and enable/disable controls.
|
||||
- `channels.<provider>.configWrites` gates config mutations per channel (default: true).
|
||||
- For multi-account channels, `channels.<provider>.accounts.<id>.configWrites` also gates writes that target that account (for example `/allowlist --config --account <id>` or `/config set channels.<provider>.accounts.<id>...`).
|
||||
- `restart: false` disables `/restart` and gateway restart tool actions. Default: `true`.
|
||||
- `ownerAllowFrom` is the explicit owner allowlist for owner-only commands/tools. It is separate from `allowFrom`.
|
||||
- `ownerDisplay: "hash"` hashes owner ids in the system prompt. Set `ownerDisplaySecret` to control hashing.
|
||||
- `allowFrom` is per-provider. When set, it is the **only** authorization source (channel allowlists/pairing and `useAccessGroups` are ignored).
|
||||
- `useAccessGroups: false` allows commands to bypass access-group policies when `allowFrom` is not set.
|
||||
- Command docs map:
|
||||
- built-in + bundled catalog: [Slash Commands](/tools/slash-commands)
|
||||
- channel-specific command surfaces: [Channels](/channels)
|
||||
- QQ Bot commands: [QQ Bot](/channels/qqbot)
|
||||
- pairing commands: [Pairing](/channels/pairing)
|
||||
- LINE card command: [LINE](/channels/line)
|
||||
- memory dreaming: [Dreaming](/concepts/dreaming)
|
||||
|
||||
</Accordion>
|
||||
|
||||
@@ -1117,6 +1156,20 @@ Optional CLI backends for text-only fallback runs (no tool calls). Useful as a b
|
||||
- Sessions supported when `sessionArg` is set.
|
||||
- Image pass-through supported when `imageArg` accepts file paths.
|
||||
|
||||
### `agents.defaults.systemPromptOverride`
|
||||
|
||||
Replace the entire OpenClaw-assembled system prompt with a fixed string. Set at the default level (`agents.defaults.systemPromptOverride`) or per agent (`agents.list[].systemPromptOverride`). Per-agent values take precedence; an empty or whitespace-only value is ignored. Useful for controlled prompt experiments.
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
systemPromptOverride: "You are a helpful assistant.",
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### `agents.defaults.heartbeat`
|
||||
|
||||
Periodic heartbeat runs.
|
||||
@@ -1129,6 +1182,7 @@ Periodic heartbeat runs.
|
||||
every: "30m", // 0m disables
|
||||
model: "openai/gpt-5.4-mini",
|
||||
includeReasoning: false,
|
||||
includeSystemPromptSection: true, // default: true; false omits the Heartbeat section from the system prompt
|
||||
lightContext: false, // default: false; true keeps only HEARTBEAT.md from workspace bootstrap files
|
||||
isolatedSession: false, // default: false; true runs each heartbeat in a fresh session (no conversation history)
|
||||
session: "main",
|
||||
@@ -1145,6 +1199,7 @@ Periodic heartbeat runs.
|
||||
```
|
||||
|
||||
- `every`: duration string (ms/s/m/h). Default: `30m` (API-key auth) or `1h` (OAuth auth). Set to `0m` to disable.
|
||||
- `includeSystemPromptSection`: when false, omits the Heartbeat section from the system prompt and skips `HEARTBEAT.md` injection into bootstrap context. Default: `true`.
|
||||
- `suppressToolErrorWarnings`: when true, suppresses tool error warning payloads during heartbeat runs.
|
||||
- `directPolicy`: direct/DM delivery policy. `allow` (default) permits direct-target delivery. `block` suppresses direct-target delivery and emits `reason=dm-blocked`.
|
||||
- `lightContext`: when true, heartbeat runs use lightweight bootstrap context and keep only `HEARTBEAT.md` from workspace bootstrap files.
|
||||
@@ -1904,7 +1959,7 @@ Batches rapid text-only messages from the same sender into a single agent turn.
|
||||
}
|
||||
```
|
||||
|
||||
- `auto` controls auto-TTS. `/tts off|always|inbound|tagged` overrides per session.
|
||||
- `auto` controls the default auto-TTS mode: `off`, `always`, `inbound`, or `tagged`. `/tts on|off` can override local prefs, and `/tts status` shows the effective state.
|
||||
- `summaryModel` overrides `agents.defaults.model.primary` for auto-summary.
|
||||
- `modelOverrides` is enabled by default; `modelOverrides.allowProvider` defaults to `false` (opt-in).
|
||||
- API keys fall back to `ELEVENLABS_API_KEY`/`XI_API_KEY` and `OPENAI_API_KEY`.
|
||||
@@ -2259,7 +2314,7 @@ Experimental built-in tool flags. Default off unless a runtime-specific auto-ena
|
||||
Notes:
|
||||
|
||||
- `planTool`: enables the structured `update_plan` tool for non-trivial multi-step work tracking.
|
||||
- Default: `false` for non-OpenAI providers. OpenAI and OpenAI Codex runs auto-enable it.
|
||||
- Default: `false` for non-OpenAI providers. OpenAI and OpenAI Codex runs auto-enable it when unset; set `false` to disable that auto-enable.
|
||||
- When enabled, the system prompt also adds usage guidance so the model only uses it for substantial work and keeps at most one step `in_progress`.
|
||||
|
||||
### `agents.defaults.subagents`
|
||||
@@ -2676,6 +2731,12 @@ See [Local Models](/gateway/local-models). TL;DR: run a large local model via LM
|
||||
- `enabled`: master dreaming switch (default `false`).
|
||||
- `frequency`: cron cadence for each full dreaming sweep (`"0 3 * * *"` by default).
|
||||
- phase policy and thresholds are implementation details (not user-facing config keys).
|
||||
- Full memory config lives in [Memory configuration reference](/reference/memory-config):
|
||||
- `agents.defaults.memorySearch.*`
|
||||
- `memory.backend`
|
||||
- `memory.citations`
|
||||
- `memory.qmd.*`
|
||||
- `plugins.entries.memory-core.config.dreaming`
|
||||
- Enabled Claude bundle plugins can also contribute embedded Pi defaults from `settings.json`; OpenClaw applies those as sanitized agent settings, not as raw OpenClaw config patches.
|
||||
- `plugins.slots.memory`: pick the active memory plugin id, or `"none"` to disable memory plugins.
|
||||
- `plugins.slots.contextEngine`: pick the active context engine plugin id; defaults to `"legacy"` unless you install and select another engine.
|
||||
|
||||
@@ -72,6 +72,8 @@ Schema tooling notes:
|
||||
|
||||
- `openclaw config schema` prints the same JSON Schema family used by Control UI
|
||||
and config validation.
|
||||
- Treat that schema output as the canonical machine-readable contract for
|
||||
`openclaw.json`; this overview and the configuration reference summarize it.
|
||||
- Field `title` and `description` values are carried into the schema output for
|
||||
editor and form tooling.
|
||||
- Nested object, wildcard (`*`), and array-item (`[]`) entries inherit the same
|
||||
@@ -84,6 +86,8 @@ Schema tooling notes:
|
||||
summaries for drill-down tooling.
|
||||
- Runtime plugin/channel schemas are merged in when the gateway can load the
|
||||
current manifest registry.
|
||||
- `pnpm config:docs:check` detects drift between docs-facing config baseline
|
||||
artifacts and the current schema surface.
|
||||
|
||||
When validation fails:
|
||||
|
||||
|
||||
@@ -66,6 +66,7 @@ cat ~/.openclaw/openclaw.json
|
||||
- Talk config migration from legacy flat `talk.*` fields into `talk.provider` + `talk.providers.<provider>`.
|
||||
- Browser migration checks for legacy Chrome extension configs and Chrome MCP readiness.
|
||||
- OpenCode provider override warnings (`models.providers.opencode` / `models.providers.opencode-go`).
|
||||
- Codex OAuth shadowing warnings (`models.providers.openai-codex`).
|
||||
- OAuth TLS prerequisites check for OpenAI Codex OAuth profiles.
|
||||
- Legacy on-disk state migration (sessions/agent dir/WhatsApp auth).
|
||||
- Legacy plugin manifest contract key migration (`speechProviders`, `realtimeTranscriptionProviders`, `realtimeVoiceProviders`, `mediaUnderstandingProviders`, `imageGenerationProviders`, `videoGenerationProviders`, `webFetchProviders`, `webSearchProviders` → `contracts`).
|
||||
@@ -212,6 +213,16 @@ doctor prints platform-specific fix guidance. On macOS with a Homebrew Node, the
|
||||
fix is usually `brew postinstall ca-certificates`. With `--deep`, the probe runs
|
||||
even if the gateway is healthy.
|
||||
|
||||
### 2c) Codex OAuth provider overrides
|
||||
|
||||
If you previously added legacy OpenAI transport settings under
|
||||
`models.providers.openai-codex`, they can shadow the built-in Codex OAuth
|
||||
provider path that newer releases use automatically. Doctor warns when it sees
|
||||
those old transport settings alongside Codex OAuth so you can remove or rewrite
|
||||
the stale transport override and get the built-in routing/fallback behavior
|
||||
back. Custom proxies and header-only overrides are still supported and do not
|
||||
trigger this warning.
|
||||
|
||||
### 3) Legacy state migrations (disk layout)
|
||||
|
||||
Doctor can migrate older on-disk layouts into the current structure:
|
||||
|
||||
@@ -54,7 +54,10 @@ Example config:
|
||||
- Prompt body (configurable via `agents.defaults.heartbeat.prompt`):
|
||||
`Read HEARTBEAT.md if it exists (workspace context). Follow it strictly. Do not infer or repeat old tasks from prior chats. If nothing needs attention, reply HEARTBEAT_OK.`
|
||||
- The heartbeat prompt is sent **verbatim** as the user message. The system
|
||||
prompt includes a “Heartbeat” section and the run is flagged internally.
|
||||
prompt includes a “Heartbeat” section only when heartbeats are enabled for the
|
||||
default agent, and the run is flagged internally.
|
||||
- When heartbeats are disabled with `0m`, normal runs also omit `HEARTBEAT.md`
|
||||
from bootstrap context so the model does not see heartbeat-only instructions.
|
||||
- Active hours (`heartbeat.activeHours`) are checked in the configured timezone.
|
||||
Outside the window, heartbeats are skipped until the next tick inside the window.
|
||||
|
||||
@@ -330,6 +333,11 @@ If a `HEARTBEAT.md` file exists in the workspace, the default prompt tells the
|
||||
agent to read it. Think of it as your “heartbeat checklist”: small, stable, and
|
||||
safe to include every 30 minutes.
|
||||
|
||||
On normal runs, `HEARTBEAT.md` is only injected when heartbeat guidance is
|
||||
enabled for the default agent. Disabling the heartbeat cadence with `0m` or
|
||||
setting `includeSystemPromptSection: false` omits it from normal bootstrap
|
||||
context.
|
||||
|
||||
If `HEARTBEAT.md` exists but is effectively empty (only blank lines and markdown
|
||||
headers like `# Heading`), OpenClaw skips the heartbeat run to save API calls.
|
||||
That skip is reported as `reason=empty-heartbeat-file`.
|
||||
|
||||
@@ -21,6 +21,32 @@ Use these when a task is clearly tied to a script; otherwise prefer the CLI.
|
||||
|
||||
Auth monitoring is covered in [Authentication](/gateway/authentication). The scripts under `scripts/` are optional extras for systemd/Termux phone workflows.
|
||||
|
||||
## GitHub read helper
|
||||
|
||||
Use `scripts/gh-read` when you want `gh` to use a GitHub App installation token for repo-scoped read calls while leaving normal `gh` on your personal login for write actions.
|
||||
|
||||
Required env:
|
||||
|
||||
- `OPENCLAW_GH_READ_APP_ID`
|
||||
- `OPENCLAW_GH_READ_PRIVATE_KEY_FILE`
|
||||
|
||||
Optional env:
|
||||
|
||||
- `OPENCLAW_GH_READ_INSTALLATION_ID` when you want to skip repo-based installation lookup
|
||||
- `OPENCLAW_GH_READ_PERMISSIONS` as a comma-separated override for the read permission subset to request
|
||||
|
||||
Repo resolution order:
|
||||
|
||||
- `gh ... -R owner/repo`
|
||||
- `GH_REPO`
|
||||
- `git remote origin`
|
||||
|
||||
Examples:
|
||||
|
||||
- `scripts/gh-read pr view 123`
|
||||
- `scripts/gh-read run list -R openclaw/openclaw`
|
||||
- `scripts/gh-read api repos/openclaw/openclaw/pulls/123`
|
||||
|
||||
## When adding scripts
|
||||
|
||||
- Keep scripts focused and documented.
|
||||
|
||||
357
docs/plugins/memory-wiki.md
Normal file
357
docs/plugins/memory-wiki.md
Normal file
@@ -0,0 +1,357 @@
|
||||
---
|
||||
summary: "memory-wiki: compiled knowledge vault with provenance, claims, dashboards, and bridge mode"
|
||||
read_when:
|
||||
- You want persistent knowledge beyond plain MEMORY.md notes
|
||||
- You are configuring the bundled memory-wiki plugin
|
||||
- You want to understand wiki_search, wiki_get, or bridge mode
|
||||
title: "Memory Wiki"
|
||||
---
|
||||
|
||||
# Memory Wiki
|
||||
|
||||
`memory-wiki` is a bundled plugin that turns durable memory into a compiled
|
||||
knowledge vault.
|
||||
|
||||
It does **not** replace the active memory plugin. The active memory plugin still
|
||||
owns recall, promotion, indexing, and dreaming. `memory-wiki` sits beside it
|
||||
and compiles durable knowledge into a navigable wiki with deterministic pages,
|
||||
structured claims, provenance, dashboards, and machine-readable digests.
|
||||
|
||||
Use it when you want memory to behave more like a maintained knowledge layer and
|
||||
less like a pile of Markdown files.
|
||||
|
||||
## What it adds
|
||||
|
||||
- A dedicated wiki vault with deterministic page layout
|
||||
- Structured claim and evidence metadata, not just prose
|
||||
- Page-level provenance, confidence, contradictions, and open questions
|
||||
- Compiled digests for agent/runtime consumers
|
||||
- Wiki-native search/get/apply/lint tools
|
||||
- Optional bridge mode that imports public artifacts from the active memory plugin
|
||||
- Optional Obsidian-friendly render mode and CLI integration
|
||||
|
||||
## How it fits with memory
|
||||
|
||||
Think of the split like this:
|
||||
|
||||
| Layer | Owns |
|
||||
| ------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
|
||||
| Active memory plugin (`memory-core`, QMD, Honcho, etc.) | Recall, semantic search, promotion, dreaming, memory runtime |
|
||||
| `memory-wiki` | Compiled wiki pages, provenance-rich syntheses, dashboards, wiki-specific search/get/apply |
|
||||
|
||||
If the active memory plugin exposes shared recall artifacts, OpenClaw can search
|
||||
both layers in one pass with `memory_search corpus=all`.
|
||||
|
||||
When you need wiki-specific ranking, provenance, or direct page access, use the
|
||||
wiki-native tools instead.
|
||||
|
||||
## Vault modes
|
||||
|
||||
`memory-wiki` supports three vault modes:
|
||||
|
||||
### `isolated`
|
||||
|
||||
Own vault, own sources, no dependency on `memory-core`.
|
||||
|
||||
Use this when you want the wiki to be its own curated knowledge store.
|
||||
|
||||
### `bridge`
|
||||
|
||||
Reads public memory artifacts and memory events from the active memory plugin
|
||||
through public plugin SDK seams.
|
||||
|
||||
Use this when you want the wiki to compile and organize the memory plugin's
|
||||
exported artifacts without reaching into private plugin internals.
|
||||
|
||||
Bridge mode can index:
|
||||
|
||||
- exported memory artifacts
|
||||
- dream reports
|
||||
- daily notes
|
||||
- memory root files
|
||||
- memory event logs
|
||||
|
||||
### `unsafe-local`
|
||||
|
||||
Explicit same-machine escape hatch for local private paths.
|
||||
|
||||
This mode is intentionally experimental and non-portable. Use it only when you
|
||||
understand the trust boundary and specifically need local filesystem access that
|
||||
bridge mode cannot provide.
|
||||
|
||||
## Vault layout
|
||||
|
||||
The plugin initializes a vault like this:
|
||||
|
||||
```text
|
||||
<vault>/
|
||||
AGENTS.md
|
||||
WIKI.md
|
||||
index.md
|
||||
inbox.md
|
||||
entities/
|
||||
concepts/
|
||||
syntheses/
|
||||
sources/
|
||||
reports/
|
||||
_attachments/
|
||||
_views/
|
||||
.openclaw-wiki/
|
||||
```
|
||||
|
||||
Managed content stays inside generated blocks. Human note blocks are preserved.
|
||||
|
||||
The main page groups are:
|
||||
|
||||
- `sources/` for imported raw material and bridge-backed pages
|
||||
- `entities/` for durable things, people, systems, projects, and objects
|
||||
- `concepts/` for ideas, abstractions, patterns, and policies
|
||||
- `syntheses/` for compiled summaries and maintained rollups
|
||||
- `reports/` for generated dashboards
|
||||
|
||||
## Structured claims and evidence
|
||||
|
||||
Pages can carry structured `claims` frontmatter, not just freeform text.
|
||||
|
||||
Each claim can include:
|
||||
|
||||
- `id`
|
||||
- `text`
|
||||
- `status`
|
||||
- `confidence`
|
||||
- `evidence[]`
|
||||
- `updatedAt`
|
||||
|
||||
Evidence entries can include:
|
||||
|
||||
- `sourceId`
|
||||
- `path`
|
||||
- `lines`
|
||||
- `weight`
|
||||
- `note`
|
||||
- `updatedAt`
|
||||
|
||||
This is what makes the wiki act more like a belief layer than a passive note
|
||||
dump. Claims can be tracked, scored, contested, and resolved back to sources.
|
||||
|
||||
## 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.
|
||||
|
||||
Compiled output also powers:
|
||||
|
||||
- first-pass wiki indexing for search/get flows
|
||||
- claim-id lookup back to owning pages
|
||||
- compact prompt supplements
|
||||
- report/dashboard generation
|
||||
|
||||
## Dashboards and health reports
|
||||
|
||||
When `render.createDashboards` is enabled, compile maintains dashboards under
|
||||
`reports/`.
|
||||
|
||||
Built-in reports include:
|
||||
|
||||
- `reports/open-questions.md`
|
||||
- `reports/contradictions.md`
|
||||
- `reports/low-confidence.md`
|
||||
- `reports/claim-health.md`
|
||||
- `reports/stale-pages.md`
|
||||
|
||||
These reports track things like:
|
||||
|
||||
- contradiction note clusters
|
||||
- competing claim clusters
|
||||
- claims missing structured evidence
|
||||
- low-confidence pages and claims
|
||||
- stale or unknown freshness
|
||||
- pages with unresolved questions
|
||||
|
||||
## Search and retrieval
|
||||
|
||||
`memory-wiki` supports two search backends:
|
||||
|
||||
- `shared`: use the shared memory search flow when available
|
||||
- `local`: search the wiki locally
|
||||
|
||||
It also supports three corpora:
|
||||
|
||||
- `wiki`
|
||||
- `memory`
|
||||
- `all`
|
||||
|
||||
Important behavior:
|
||||
|
||||
- `wiki_search` and `wiki_get` use compiled digests as a first pass when possible
|
||||
- claim ids can resolve back to the owning page
|
||||
- contested/stale/fresh claims influence ranking
|
||||
- provenance labels can survive into results
|
||||
|
||||
Practical rule:
|
||||
|
||||
- use `memory_search corpus=all` for one broad recall pass
|
||||
- use `wiki_search` + `wiki_get` when you care about wiki-specific ranking,
|
||||
provenance, or page-level belief structure
|
||||
|
||||
## Agent tools
|
||||
|
||||
The plugin registers these tools:
|
||||
|
||||
- `wiki_status`
|
||||
- `wiki_search`
|
||||
- `wiki_get`
|
||||
- `wiki_apply`
|
||||
- `wiki_lint`
|
||||
|
||||
What they do:
|
||||
|
||||
- `wiki_status`: current vault mode, health, Obsidian CLI availability
|
||||
- `wiki_search`: search wiki pages and, when configured, shared memory corpora
|
||||
- `wiki_get`: read a wiki page by id/path or fall back to shared memory corpus
|
||||
- `wiki_apply`: narrow synthesis/metadata mutations without freeform page surgery
|
||||
- `wiki_lint`: structural checks, provenance gaps, contradictions, open questions
|
||||
|
||||
The plugin also registers a non-exclusive memory corpus supplement, so shared
|
||||
`memory_search` and `memory_get` can reach the wiki when the active memory
|
||||
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`.
|
||||
|
||||
That snapshot is intentionally small and high-signal:
|
||||
|
||||
- top pages only
|
||||
- top claims only
|
||||
- contradiction count
|
||||
- question count
|
||||
- confidence/freshness qualifiers
|
||||
|
||||
This is opt-in because it changes prompt shape and is mainly useful for context
|
||||
engines or legacy prompt assembly that explicitly consume memory supplements.
|
||||
|
||||
## Configuration
|
||||
|
||||
Put config under `plugins.entries.memory-wiki.config`:
|
||||
|
||||
```json5
|
||||
{
|
||||
plugins: {
|
||||
entries: {
|
||||
"memory-wiki": {
|
||||
enabled: true,
|
||||
config: {
|
||||
vaultMode: "isolated",
|
||||
vault: {
|
||||
path: "~/.openclaw/wiki/main",
|
||||
renderMode: "obsidian",
|
||||
},
|
||||
obsidian: {
|
||||
enabled: true,
|
||||
useOfficialCli: true,
|
||||
vaultName: "OpenClaw Wiki",
|
||||
openAfterWrites: false,
|
||||
},
|
||||
bridge: {
|
||||
enabled: false,
|
||||
readMemoryArtifacts: true,
|
||||
indexDreamReports: true,
|
||||
indexDailyNotes: true,
|
||||
indexMemoryRoot: true,
|
||||
followMemoryEvents: true,
|
||||
},
|
||||
ingest: {
|
||||
autoCompile: true,
|
||||
maxConcurrentJobs: 1,
|
||||
allowUrlIngest: true,
|
||||
},
|
||||
search: {
|
||||
backend: "shared",
|
||||
corpus: "wiki",
|
||||
},
|
||||
context: {
|
||||
includeCompiledDigestPrompt: false,
|
||||
},
|
||||
render: {
|
||||
preserveHumanBlocks: true,
|
||||
createBacklinks: true,
|
||||
createDashboards: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Key toggles:
|
||||
|
||||
- `vaultMode`: `isolated`, `bridge`, `unsafe-local`
|
||||
- `vault.renderMode`: `native` or `obsidian`
|
||||
- `bridge.readMemoryArtifacts`: import active memory plugin public artifacts
|
||||
- `bridge.followMemoryEvents`: include event logs in bridge mode
|
||||
- `search.backend`: `shared` or `local`
|
||||
- `search.corpus`: `wiki`, `memory`, or `all`
|
||||
- `context.includeCompiledDigestPrompt`: append compact digest snapshot to memory prompt sections
|
||||
- `render.createBacklinks`: generate deterministic related blocks
|
||||
- `render.createDashboards`: generate dashboard pages
|
||||
|
||||
## CLI
|
||||
|
||||
`memory-wiki` also exposes a top-level CLI surface:
|
||||
|
||||
```bash
|
||||
openclaw wiki status
|
||||
openclaw wiki doctor
|
||||
openclaw wiki init
|
||||
openclaw wiki ingest ./notes/alpha.md
|
||||
openclaw wiki compile
|
||||
openclaw wiki lint
|
||||
openclaw wiki search "alpha"
|
||||
openclaw wiki get entity.alpha
|
||||
openclaw wiki apply synthesis "Alpha Summary" --body "..." --source-id source.alpha
|
||||
openclaw wiki bridge import
|
||||
openclaw wiki obsidian status
|
||||
```
|
||||
|
||||
See [CLI: wiki](/cli/wiki) for the full command reference.
|
||||
|
||||
## Obsidian support
|
||||
|
||||
When `vault.renderMode` is `obsidian`, the plugin writes Obsidian-friendly
|
||||
Markdown and can optionally use the official `obsidian` CLI.
|
||||
|
||||
Supported workflows include:
|
||||
|
||||
- status probing
|
||||
- vault search
|
||||
- opening a page
|
||||
- invoking an Obsidian command
|
||||
- jumping to the daily note
|
||||
|
||||
This is optional. The wiki still works in native mode without Obsidian.
|
||||
|
||||
## Recommended workflow
|
||||
|
||||
1. Keep your active memory plugin for recall/promotion/dreaming.
|
||||
2. Enable `memory-wiki`.
|
||||
3. Start with `isolated` mode unless you explicitly want bridge mode.
|
||||
4. Use `wiki_search` / `wiki_get` when provenance matters.
|
||||
5. Use `wiki_apply` for narrow syntheses or metadata updates.
|
||||
6. Run `wiki_lint` after meaningful changes.
|
||||
7. Turn on dashboards if you want stale/contradiction visibility.
|
||||
|
||||
## Related docs
|
||||
|
||||
- [Memory Overview](/concepts/memory)
|
||||
- [CLI: memory](/cli/memory)
|
||||
- [CLI: wiki](/cli/wiki)
|
||||
- [Plugin SDK overview](/plugins/sdk-overview)
|
||||
@@ -262,6 +262,7 @@ Current bundled provider examples:
|
||||
| `plugin-sdk/request-url` | Request URL helpers | Extract string URLs from request-like inputs |
|
||||
| `plugin-sdk/run-command` | Timed command helpers | Timed command runner with normalized stdout/stderr |
|
||||
| `plugin-sdk/param-readers` | Param readers | Common tool/CLI param readers |
|
||||
| `plugin-sdk/tool-payload` | Tool payload extraction | Extract normalized payloads from tool result objects |
|
||||
| `plugin-sdk/tool-send` | Tool send extraction | Extract canonical send target fields from tool args |
|
||||
| `plugin-sdk/temp-path` | Temp path helpers | Shared temp-download path helpers |
|
||||
| `plugin-sdk/logging-core` | Logging helpers | Subsystem logger and redaction helpers |
|
||||
@@ -279,7 +280,8 @@ Current bundled provider examples:
|
||||
| `plugin-sdk/provider-onboard` | Provider onboarding patches | Onboarding config helpers |
|
||||
| `plugin-sdk/provider-http` | Provider HTTP helpers | Generic provider HTTP/endpoint capability helpers |
|
||||
| `plugin-sdk/provider-web-fetch` | Provider web-fetch helpers | Web-fetch provider registration/cache helpers |
|
||||
| `plugin-sdk/provider-web-search-contract` | Provider web-search contract helpers | Narrow web-search config/credential contract helpers such as `enablePluginInConfig`, `resolveProviderWebSearchPluginConfig`, and scoped credential setters/getters |
|
||||
| `plugin-sdk/provider-web-search-config-contract` | Provider web-search config helpers | Narrow web-search config/credential helpers for providers that do not need plugin-enable wiring |
|
||||
| `plugin-sdk/provider-web-search-contract` | Provider web-search contract helpers | Narrow web-search config/credential contract helpers such as `createWebSearchProviderContractFields`, `enablePluginInConfig`, `resolveProviderWebSearchPluginConfig`, and scoped credential setters/getters |
|
||||
| `plugin-sdk/provider-web-search` | Provider web-search helpers | Web-search provider registration/cache/runtime helpers |
|
||||
| `plugin-sdk/provider-tools` | Provider tool/schema compat helpers | `ProviderToolCompatFamily`, `buildProviderToolCompatFamilyHooks`, Gemini schema cleanup + diagnostics, and xAI compat helpers such as `resolveXaiModelCompatPatch` / `applyXaiModelCompat` |
|
||||
| `plugin-sdk/provider-usage` | Provider usage helpers | `fetchClaudeUsage`, `fetchGeminiUsage`, `fetchGithubCopilotUsage`, and other provider usage helpers |
|
||||
|
||||
@@ -135,7 +135,8 @@ explicitly promotes one as public.
|
||||
| `plugin-sdk/provider-http` | Generic provider HTTP/endpoint capability helpers |
|
||||
| `plugin-sdk/provider-web-fetch-contract` | Narrow web-fetch config/selection contract helpers such as `enablePluginInConfig` and `WebFetchProviderPlugin` |
|
||||
| `plugin-sdk/provider-web-fetch` | Web-fetch provider registration/cache helpers |
|
||||
| `plugin-sdk/provider-web-search-contract` | Narrow web-search config/credential contract helpers such as `enablePluginInConfig`, `resolveProviderWebSearchPluginConfig`, and scoped credential setters/getters |
|
||||
| `plugin-sdk/provider-web-search-config-contract` | Narrow web-search config/credential helpers for providers that do not need plugin-enable wiring |
|
||||
| `plugin-sdk/provider-web-search-contract` | Narrow web-search config/credential contract helpers such as `createWebSearchProviderContractFields`, `enablePluginInConfig`, `resolveProviderWebSearchPluginConfig`, and scoped credential setters/getters |
|
||||
| `plugin-sdk/provider-web-search` | Web-search provider registration/cache/runtime helpers |
|
||||
| `plugin-sdk/provider-tools` | `ProviderToolCompatFamily`, `buildProviderToolCompatFamilyHooks`, Gemini schema cleanup + diagnostics, and xAI compat helpers such as `resolveXaiModelCompatPatch` / `applyXaiModelCompat` |
|
||||
| `plugin-sdk/provider-usage` | `fetchClaudeUsage` and similar |
|
||||
@@ -200,6 +201,7 @@ explicitly promotes one as public.
|
||||
| `plugin-sdk/request-url` | Extract string URLs from fetch/request-like inputs |
|
||||
| `plugin-sdk/run-command` | Timed command runner with normalized stdout/stderr results |
|
||||
| `plugin-sdk/param-readers` | Common tool/CLI param readers |
|
||||
| `plugin-sdk/tool-payload` | Extract normalized payloads from tool result objects |
|
||||
| `plugin-sdk/tool-send` | Extract canonical send target fields from tool args |
|
||||
| `plugin-sdk/temp-path` | Shared temp-download path helpers |
|
||||
| `plugin-sdk/logging-core` | Subsystem logger and redaction helpers |
|
||||
@@ -387,13 +389,13 @@ AI CLI backend such as `codex-cli`.
|
||||
|
||||
### Exclusive slots
|
||||
|
||||
| Method | What it registers |
|
||||
| ------------------------------------------ | ------------------------------------- |
|
||||
| `api.registerContextEngine(id, factory)` | Context engine (one active at a time) |
|
||||
| `api.registerMemoryCapability(capability)` | Unified memory capability |
|
||||
| `api.registerMemoryPromptSection(builder)` | Memory prompt section builder |
|
||||
| `api.registerMemoryFlushPlan(resolver)` | Memory flush plan resolver |
|
||||
| `api.registerMemoryRuntime(runtime)` | Memory runtime adapter |
|
||||
| Method | What it registers |
|
||||
| ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `api.registerContextEngine(id, factory)` | Context engine (one active at a time). The `assemble()` callback receives `availableTools` and `citationsMode` so the engine can tailor prompt additions. |
|
||||
| `api.registerMemoryCapability(capability)` | Unified memory capability |
|
||||
| `api.registerMemoryPromptSection(builder)` | Memory prompt section builder |
|
||||
| `api.registerMemoryFlushPlan(resolver)` | Memory flush plan resolver |
|
||||
| `api.registerMemoryRuntime(runtime)` | Memory runtime adapter |
|
||||
|
||||
### Memory embedding adapters
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ openclaw onboard --auth-choice zai-cn
|
||||
```json5
|
||||
{
|
||||
env: { ZAI_API_KEY: "sk-..." },
|
||||
agents: { defaults: { model: { primary: "zai/glm-5" } } },
|
||||
agents: { defaults: { model: { primary: "zai/glm-5.1" } } },
|
||||
}
|
||||
```
|
||||
|
||||
@@ -64,5 +64,5 @@ OpenClaw currently seeds the bundled `zai` provider with these GLM refs:
|
||||
## Notes
|
||||
|
||||
- GLM versions and availability can change; check Z.AI's docs for the latest.
|
||||
- Default bundled model ref is `zai/glm-5`.
|
||||
- Default bundled model ref is `zai/glm-5.1`.
|
||||
- For provider details, see [/providers/zai](/providers/zai).
|
||||
|
||||
@@ -98,6 +98,9 @@ Gemini CLI JSON usage notes:
|
||||
| Video understanding | Yes |
|
||||
| Web search (Grounding) | Yes |
|
||||
| Thinking/reasoning | Yes (Gemini 3.1+) |
|
||||
| Gemma 4 models | Yes |
|
||||
|
||||
Gemma 4 models (for example `gemma-4-26b-a4b-it`) support thinking mode. OpenClaw rewrites `thinkingBudget` to a supported Google `thinkingLevel` for Gemma 4. Setting thinking to `off` preserves thinking disabled instead of mapping to `MINIMAL`.
|
||||
|
||||
## Direct Gemini cache reuse
|
||||
|
||||
|
||||
@@ -119,7 +119,8 @@ openclaw models set ollama/gemma4
|
||||
When you set `OLLAMA_API_KEY` (or an auth profile) and **do not** define `models.providers.ollama`, OpenClaw discovers models from the local Ollama instance at `http://127.0.0.1:11434`:
|
||||
|
||||
- Queries `/api/tags`
|
||||
- Uses best-effort `/api/show` lookups to read `contextWindow` when available
|
||||
- Uses best-effort `/api/show` lookups to read `contextWindow` and detect capabilities (including vision) when available
|
||||
- Models with a `vision` capability reported by `/api/show` are marked as image-capable (`input: ["text", "image"]`), so OpenClaw auto-injects images into the prompt for those models
|
||||
- Marks `reasoning` with a model-name heuristic (`r1`, `reasoning`, `think`)
|
||||
- Sets `maxTokens` to the default Ollama max-token cap used by OpenClaw
|
||||
- Sets all costs to `0`
|
||||
|
||||
@@ -36,7 +36,7 @@ openclaw onboard --auth-choice zai-cn
|
||||
```json5
|
||||
{
|
||||
env: { ZAI_API_KEY: "sk-..." },
|
||||
agents: { defaults: { model: { primary: "zai/glm-5" } } },
|
||||
agents: { defaults: { model: { primary: "zai/glm-5.1" } } },
|
||||
}
|
||||
```
|
||||
|
||||
@@ -65,7 +65,7 @@ OpenClaw currently seeds the bundled `zai` provider with:
|
||||
## Notes
|
||||
|
||||
- GLM models are available as `zai/<model>` (example: `zai/glm-5`).
|
||||
- Default bundled model ref: `zai/glm-5`
|
||||
- Default bundled model ref: `zai/glm-5.1`
|
||||
- Unknown `glm-5*` ids still forward-resolve on the bundled provider path by
|
||||
synthesizing provider-owned metadata from the `glm-4.7` template when the id
|
||||
matches the current GLM-5 family shape.
|
||||
|
||||
530
docs/refactor/qa.md
Normal file
530
docs/refactor/qa.md
Normal file
@@ -0,0 +1,530 @@
|
||||
# QA Refactor
|
||||
|
||||
Status: foundational migration landed.
|
||||
|
||||
## Goal
|
||||
|
||||
Move OpenClaw QA from a split-definition model to a single source of truth:
|
||||
|
||||
- scenario metadata
|
||||
- prompts sent to the model
|
||||
- setup and teardown
|
||||
- harness logic
|
||||
- assertions and success criteria
|
||||
- artifacts and report hints
|
||||
|
||||
The desired end state is a generic QA harness that loads powerful scenario definition files instead of hardcoding most behavior in TypeScript.
|
||||
|
||||
## Current State
|
||||
|
||||
Primary source of truth now lives in `qa/scenarios/index.md` plus one file per
|
||||
scenario under `qa/scenarios/*.md`.
|
||||
|
||||
Implemented:
|
||||
|
||||
- `qa/scenarios/index.md`
|
||||
- canonical QA pack metadata
|
||||
- operator identity
|
||||
- kickoff mission
|
||||
- `qa/scenarios/*.md`
|
||||
- one markdown file per scenario
|
||||
- scenario metadata
|
||||
- handler bindings
|
||||
- scenario-specific execution config
|
||||
- `extensions/qa-lab/src/scenario-catalog.ts`
|
||||
- markdown pack parser + zod validation
|
||||
- `extensions/qa-lab/src/qa-agent-bootstrap.ts`
|
||||
- plan rendering from the markdown pack
|
||||
- `extensions/qa-lab/src/qa-agent-workspace.ts`
|
||||
- seeds generated compatibility files plus `QA_SCENARIOS.md`
|
||||
- `extensions/qa-lab/src/suite.ts`
|
||||
- selects executable scenarios through markdown-defined handler bindings
|
||||
- QA bus protocol + UI
|
||||
- generic inline attachments for image/video/audio/file rendering
|
||||
|
||||
Remaining split surfaces:
|
||||
|
||||
- `extensions/qa-lab/src/suite.ts`
|
||||
- still owns most executable custom handler logic
|
||||
- `extensions/qa-lab/src/report.ts`
|
||||
- still derives report structure from runtime outputs
|
||||
|
||||
So the source-of-truth split is fixed, but execution is still mostly handler-backed rather than fully declarative.
|
||||
|
||||
## What The Real Scenario Surface Looks Like
|
||||
|
||||
Reading the current suite shows a few distinct scenario classes.
|
||||
|
||||
### Simple interaction
|
||||
|
||||
- channel baseline
|
||||
- DM baseline
|
||||
- threaded follow-up
|
||||
- model switch
|
||||
- approval followthrough
|
||||
- reaction/edit/delete
|
||||
|
||||
### Config and runtime mutation
|
||||
|
||||
- config patch skill disable
|
||||
- config apply restart wake-up
|
||||
- config restart capability flip
|
||||
- runtime inventory drift check
|
||||
|
||||
### Filesystem and repo assertions
|
||||
|
||||
- source/docs discovery report
|
||||
- build Lobster Invaders
|
||||
- generated image artifact lookup
|
||||
|
||||
### Memory orchestration
|
||||
|
||||
- memory recall
|
||||
- memory tools in channel context
|
||||
- memory failure fallback
|
||||
- session memory ranking
|
||||
- thread memory isolation
|
||||
- memory dreaming sweep
|
||||
|
||||
### Tool and plugin integration
|
||||
|
||||
- MCP plugin-tools call
|
||||
- skill visibility
|
||||
- skill hot install
|
||||
- native image generation
|
||||
- image roundtrip
|
||||
- image understanding from attachment
|
||||
|
||||
### Multi-turn and multi-actor
|
||||
|
||||
- subagent handoff
|
||||
- subagent fanout synthesis
|
||||
- restart recovery style flows
|
||||
|
||||
These categories matter because they drive DSL requirements. A flat list of prompt + expected text is not enough.
|
||||
|
||||
## Direction
|
||||
|
||||
### Single source of truth
|
||||
|
||||
Use `qa/scenarios/index.md` plus `qa/scenarios/*.md` as the authored source of
|
||||
truth.
|
||||
|
||||
The pack should stay:
|
||||
|
||||
- human-readable in review
|
||||
- machine-parseable
|
||||
- rich enough to drive:
|
||||
- suite execution
|
||||
- QA workspace bootstrap
|
||||
- QA Lab UI metadata
|
||||
- docs/discovery prompts
|
||||
- report generation
|
||||
|
||||
### Preferred authoring format
|
||||
|
||||
Use markdown as the top-level format, with structured YAML inside it.
|
||||
|
||||
Recommended shape:
|
||||
|
||||
- YAML frontmatter
|
||||
- id
|
||||
- title
|
||||
- surface
|
||||
- tags
|
||||
- docs refs
|
||||
- code refs
|
||||
- model/provider overrides
|
||||
- prerequisites
|
||||
- prose sections
|
||||
- objective
|
||||
- notes
|
||||
- debugging hints
|
||||
- fenced YAML blocks
|
||||
- setup
|
||||
- steps
|
||||
- assertions
|
||||
- cleanup
|
||||
|
||||
This gives:
|
||||
|
||||
- better PR readability than giant JSON
|
||||
- richer context than pure YAML
|
||||
- strict parsing and zod validation
|
||||
|
||||
Raw JSON is acceptable only as an intermediate generated form.
|
||||
|
||||
## Proposed Scenario File Shape
|
||||
|
||||
Example:
|
||||
|
||||
````md
|
||||
---
|
||||
id: image-generation-roundtrip
|
||||
title: Image generation roundtrip
|
||||
surface: image
|
||||
tags: [media, image, roundtrip]
|
||||
models:
|
||||
primary: openai/gpt-5.4
|
||||
requires:
|
||||
tools: [image_generate]
|
||||
plugins: [openai, qa-channel]
|
||||
docsRefs:
|
||||
- docs/help/testing.md
|
||||
- docs/concepts/model-providers.md
|
||||
codeRefs:
|
||||
- extensions/qa-lab/src/suite.ts
|
||||
- src/gateway/chat-attachments.ts
|
||||
---
|
||||
|
||||
# Objective
|
||||
|
||||
Verify generated media is reattached on the follow-up turn.
|
||||
|
||||
# Setup
|
||||
|
||||
```yaml scenario.setup
|
||||
- action: config.patch
|
||||
patch:
|
||||
agents:
|
||||
defaults:
|
||||
imageGenerationModel:
|
||||
primary: openai/gpt-image-1
|
||||
- action: session.create
|
||||
key: agent:qa:image-roundtrip
|
||||
```
|
||||
|
||||
# Steps
|
||||
|
||||
```yaml scenario.steps
|
||||
- action: agent.send
|
||||
session: agent:qa:image-roundtrip
|
||||
message: |
|
||||
Image generation check: generate a QA lighthouse image and summarize it in one short sentence.
|
||||
- action: artifact.capture
|
||||
kind: generated-image
|
||||
promptSnippet: Image generation check
|
||||
saveAs: lighthouseImage
|
||||
- action: agent.send
|
||||
session: agent:qa:image-roundtrip
|
||||
message: |
|
||||
Roundtrip image inspection check: describe the generated lighthouse attachment in one short sentence.
|
||||
attachments:
|
||||
- fromArtifact: lighthouseImage
|
||||
```
|
||||
|
||||
# Expect
|
||||
|
||||
```yaml scenario.expect
|
||||
- assert: outbound.textIncludes
|
||||
value: lighthouse
|
||||
- assert: requestLog.matches
|
||||
where:
|
||||
promptIncludes: Roundtrip image inspection check
|
||||
imageInputCountGte: 1
|
||||
- assert: artifact.exists
|
||||
ref: lighthouseImage
|
||||
```
|
||||
````
|
||||
|
||||
## Runner Capabilities The DSL Must Cover
|
||||
|
||||
Based on the current suite, the generic runner needs more than prompt execution.
|
||||
|
||||
### Environment and setup actions
|
||||
|
||||
- `bus.reset`
|
||||
- `gateway.waitHealthy`
|
||||
- `channel.waitReady`
|
||||
- `session.create`
|
||||
- `thread.create`
|
||||
- `workspace.writeSkill`
|
||||
|
||||
### Agent turn actions
|
||||
|
||||
- `agent.send`
|
||||
- `agent.wait`
|
||||
- `bus.injectInbound`
|
||||
- `bus.injectOutbound`
|
||||
|
||||
### Config and runtime actions
|
||||
|
||||
- `config.get`
|
||||
- `config.patch`
|
||||
- `config.apply`
|
||||
- `gateway.restart`
|
||||
- `tools.effective`
|
||||
- `skills.status`
|
||||
|
||||
### File and artifact actions
|
||||
|
||||
- `file.write`
|
||||
- `file.read`
|
||||
- `file.delete`
|
||||
- `file.touchTime`
|
||||
- `artifact.captureGeneratedImage`
|
||||
- `artifact.capturePath`
|
||||
|
||||
### Memory and cron actions
|
||||
|
||||
- `memory.indexForce`
|
||||
- `memory.searchCli`
|
||||
- `doctor.memory.status`
|
||||
- `cron.list`
|
||||
- `cron.run`
|
||||
- `cron.waitCompletion`
|
||||
- `sessionTranscript.write`
|
||||
|
||||
### MCP actions
|
||||
|
||||
- `mcp.callTool`
|
||||
|
||||
### Assertions
|
||||
|
||||
- `outbound.textIncludes`
|
||||
- `outbound.inThread`
|
||||
- `outbound.notInRoot`
|
||||
- `tool.called`
|
||||
- `tool.notPresent`
|
||||
- `skill.visible`
|
||||
- `skill.disabled`
|
||||
- `file.contains`
|
||||
- `memory.contains`
|
||||
- `requestLog.matches`
|
||||
- `sessionStore.matches`
|
||||
- `cron.managedPresent`
|
||||
- `artifact.exists`
|
||||
|
||||
## Variables and Artifact References
|
||||
|
||||
The DSL must support saved outputs and later references.
|
||||
|
||||
Examples from the current suite:
|
||||
|
||||
- create a thread, then reuse `threadId`
|
||||
- create a session, then reuse `sessionKey`
|
||||
- generate an image, then attach the file on the next turn
|
||||
- generate a wake marker string, then assert that it appears later
|
||||
|
||||
Needed capabilities:
|
||||
|
||||
- `saveAs`
|
||||
- `${vars.name}`
|
||||
- `${artifacts.name}`
|
||||
- typed references for paths, session keys, thread ids, markers, tool outputs
|
||||
|
||||
Without variable support, the harness will keep leaking scenario logic back into TypeScript.
|
||||
|
||||
## What Should Stay As Escape Hatches
|
||||
|
||||
A fully pure declarative runner is not realistic in phase 1.
|
||||
|
||||
Some scenarios are inherently orchestration-heavy:
|
||||
|
||||
- memory dreaming sweep
|
||||
- config apply restart wake-up
|
||||
- config restart capability flip
|
||||
- generated image artifact resolution by timestamp/path
|
||||
- discovery-report evaluation
|
||||
|
||||
These should use explicit custom handlers for now.
|
||||
|
||||
Recommended rule:
|
||||
|
||||
- 85-90% declarative
|
||||
- explicit `customHandler` steps for the hard remainder
|
||||
- named and documented custom handlers only
|
||||
- no anonymous inline code in the scenario file
|
||||
|
||||
That keeps the generic engine clean while still allowing progress.
|
||||
|
||||
## Architecture Change
|
||||
|
||||
### Current
|
||||
|
||||
Scenario markdown already is the source of truth for:
|
||||
|
||||
- suite execution
|
||||
- workspace bootstrap files
|
||||
- QA Lab UI scenario catalog
|
||||
- report metadata
|
||||
- discovery prompts
|
||||
|
||||
Generated compatibility:
|
||||
|
||||
- seeded workspace still includes `QA_KICKOFF_TASK.md`
|
||||
- seeded workspace still includes `QA_SCENARIO_PLAN.md`
|
||||
- seeded workspace now also includes `QA_SCENARIOS.md`
|
||||
|
||||
## Refactor Plan
|
||||
|
||||
### Phase 1: loader and schema
|
||||
|
||||
Done.
|
||||
|
||||
- added `qa/scenarios/index.md`
|
||||
- split scenarios into `qa/scenarios/*.md`
|
||||
- added parser for named markdown YAML pack content
|
||||
- validated with zod
|
||||
- switched consumers to the parsed pack
|
||||
- removed repo-level `qa/seed-scenarios.json` and `qa/QA_KICKOFF_TASK.md`
|
||||
|
||||
### Phase 2: generic engine
|
||||
|
||||
- split `extensions/qa-lab/src/suite.ts` into:
|
||||
- loader
|
||||
- engine
|
||||
- action registry
|
||||
- assertion registry
|
||||
- custom handlers
|
||||
- keep existing helper functions as engine operations
|
||||
|
||||
Deliverable:
|
||||
|
||||
- engine executes simple declarative scenarios
|
||||
|
||||
Start with scenarios that are mostly prompt + wait + assert:
|
||||
|
||||
- threaded follow-up
|
||||
- image understanding from attachment
|
||||
- skill visibility and invocation
|
||||
- channel baseline
|
||||
|
||||
Deliverable:
|
||||
|
||||
- first real markdown-defined scenarios shipping through the generic engine
|
||||
|
||||
### Phase 4: migrate medium scenarios
|
||||
|
||||
- image generation roundtrip
|
||||
- memory tools in channel context
|
||||
- session memory ranking
|
||||
- subagent handoff
|
||||
- subagent fanout synthesis
|
||||
|
||||
Deliverable:
|
||||
|
||||
- variables, artifacts, tool assertions, request-log assertions proven out
|
||||
|
||||
### Phase 5: keep hard scenarios on custom handlers
|
||||
|
||||
- memory dreaming sweep
|
||||
- config apply restart wake-up
|
||||
- config restart capability flip
|
||||
- runtime inventory drift
|
||||
|
||||
Deliverable:
|
||||
|
||||
- same authoring format, but with explicit custom-step blocks where needed
|
||||
|
||||
### Phase 6: delete hardcoded scenario map
|
||||
|
||||
Once the pack coverage is good enough:
|
||||
|
||||
- remove most scenario-specific TypeScript branching from `extensions/qa-lab/src/suite.ts`
|
||||
|
||||
## Fake Slack / Rich Media Support
|
||||
|
||||
The current QA bus is text-first.
|
||||
|
||||
Relevant files:
|
||||
|
||||
- `extensions/qa-channel/src/protocol.ts`
|
||||
- `extensions/qa-lab/src/bus-state.ts`
|
||||
- `extensions/qa-lab/src/bus-queries.ts`
|
||||
- `extensions/qa-lab/src/bus-server.ts`
|
||||
- `extensions/qa-lab/web/src/ui-render.ts`
|
||||
|
||||
Today the QA bus supports:
|
||||
|
||||
- text
|
||||
- reactions
|
||||
- threads
|
||||
|
||||
It does not yet model inline media attachments.
|
||||
|
||||
### Needed transport contract
|
||||
|
||||
Add a generic QA bus attachment model:
|
||||
|
||||
```ts
|
||||
type QaBusAttachment = {
|
||||
id: string;
|
||||
kind: "image" | "video" | "audio" | "file";
|
||||
mimeType: string;
|
||||
fileName?: string;
|
||||
inline?: boolean;
|
||||
url?: string;
|
||||
contentBase64?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
durationMs?: number;
|
||||
altText?: string;
|
||||
transcript?: string;
|
||||
};
|
||||
```
|
||||
|
||||
Then add `attachments?: QaBusAttachment[]` to:
|
||||
|
||||
- `QaBusMessage`
|
||||
- `QaBusInboundMessageInput`
|
||||
- `QaBusOutboundMessageInput`
|
||||
|
||||
### Why generic first
|
||||
|
||||
Do not build a Slack-only media model.
|
||||
|
||||
Instead:
|
||||
|
||||
- one generic QA transport model
|
||||
- multiple renderers on top of it
|
||||
- current QA Lab chat
|
||||
- future fake Slack web
|
||||
- any other fake transport views
|
||||
|
||||
This prevents duplicate logic and lets media scenarios stay transport-agnostic.
|
||||
|
||||
### UI work needed
|
||||
|
||||
Update the QA UI to render:
|
||||
|
||||
- inline image preview
|
||||
- inline audio player
|
||||
- inline video player
|
||||
- file attachment chip
|
||||
|
||||
The current UI can already render threads and reactions, so attachment rendering should layer onto the same message card model.
|
||||
|
||||
### Scenario work enabled by media transport
|
||||
|
||||
Once attachments flow through QA bus, we can add richer fake-chat scenarios:
|
||||
|
||||
- inline image reply in fake Slack
|
||||
- audio attachment understanding
|
||||
- video attachment understanding
|
||||
- mixed attachment ordering
|
||||
- thread reply with media retained
|
||||
|
||||
## Recommendation
|
||||
|
||||
The next implementation chunk should be:
|
||||
|
||||
1. add markdown scenario loader + zod schema
|
||||
2. generate the current catalog from markdown
|
||||
3. migrate a few simple scenarios first
|
||||
4. add generic QA bus attachment support
|
||||
5. render inline image in the QA UI
|
||||
6. then expand to audio and video
|
||||
|
||||
This is the smallest path that proves both goals:
|
||||
|
||||
- generic markdown-defined QA
|
||||
- richer fake messaging surfaces
|
||||
|
||||
## Open Questions
|
||||
|
||||
- whether scenario files should allow embedded markdown prompt templates with variable interpolation
|
||||
- whether setup/cleanup should be named sections or just ordered action lists
|
||||
- whether artifact references should be strongly typed in schema or string-based
|
||||
- whether custom handlers should live in one registry or per-surface registries
|
||||
- whether the generated JSON compatibility file should remain checked in during migration
|
||||
@@ -17,22 +17,10 @@ conceptual overviews, see:
|
||||
- [Builtin Engine](/concepts/memory-builtin) -- default SQLite backend
|
||||
- [QMD Engine](/concepts/memory-qmd) -- local-first sidecar
|
||||
- [Memory Search](/concepts/memory-search) -- search pipeline and tuning
|
||||
- [Active Memory](/concepts/active-memory) -- enabling the memory sidecar for interactive sessions
|
||||
|
||||
All memory search settings live under `agents.defaults.memorySearch` in
|
||||
`openclaw.json` unless noted otherwise.
|
||||
|
||||
If you are looking for the **active memory** feature toggle and sidecar config,
|
||||
that lives under `plugins.entries.active-memory` instead of `memorySearch`.
|
||||
|
||||
Active memory uses a two-gate model:
|
||||
|
||||
1. the plugin must be enabled and target the current agent id
|
||||
2. the request must be an eligible interactive persistent chat session
|
||||
|
||||
See [Active Memory](/concepts/active-memory) for the activation model,
|
||||
plugin-owned config, transcript persistence, and safe rollout pattern.
|
||||
|
||||
---
|
||||
|
||||
## Provider selection
|
||||
|
||||
@@ -774,6 +774,19 @@ Security and trust notes:
|
||||
Custom `mcpServers` still work as before. The built-in plugin-tools bridge is an
|
||||
additional opt-in convenience, not a replacement for generic MCP server config.
|
||||
|
||||
### Runtime timeout configuration
|
||||
|
||||
The bundled `acpx` plugin defaults embedded runtime turns to a 120-second
|
||||
timeout. This gives slower harnesses such as Gemini CLI enough time to complete
|
||||
ACP startup and initialization. Override it if your host needs a different
|
||||
runtime limit:
|
||||
|
||||
```bash
|
||||
openclaw config set plugins.entries.acpx.config.timeoutSeconds 180
|
||||
```
|
||||
|
||||
Restart the gateway after changing this value.
|
||||
|
||||
## Permission configuration
|
||||
|
||||
ACP sessions run non-interactively — there is no TTY to approve or deny file-write and shell-exec permission prompts. The acpx plugin provides two config keys that control how permissions are handled:
|
||||
|
||||
@@ -39,7 +39,10 @@ They run immediately, are stripped before the model sees the message, and the re
|
||||
mcp: false,
|
||||
plugins: false,
|
||||
debug: false,
|
||||
restart: false,
|
||||
restart: true,
|
||||
ownerAllowFrom: ["discord:123456789012345678"],
|
||||
ownerDisplay: "raw",
|
||||
ownerDisplaySecret: "${OWNER_ID_HASH_SECRET}",
|
||||
allowFrom: {
|
||||
"*": ["user1"],
|
||||
discord: ["user:123"],
|
||||
@@ -64,6 +67,10 @@ They run immediately, are stripped before the model sees the message, and the re
|
||||
- `commands.mcp` (default `false`) enables `/mcp` (reads/writes OpenClaw-managed MCP config under `mcp.servers`).
|
||||
- `commands.plugins` (default `false`) enables `/plugins` (plugin discovery/status plus install + enable/disable controls).
|
||||
- `commands.debug` (default `false`) enables `/debug` (runtime-only overrides).
|
||||
- `commands.restart` (default `true`) enables `/restart` plus gateway restart tool actions.
|
||||
- `commands.ownerAllowFrom` (optional) sets the explicit owner allowlist for owner-only command/tool surfaces. This is separate from `commands.allowFrom`.
|
||||
- `commands.ownerDisplay` controls how owner ids appear in the system prompt: `raw` or `hash`.
|
||||
- `commands.ownerDisplaySecret` optionally sets the HMAC secret used when `commands.ownerDisplay="hash"`.
|
||||
- `commands.allowFrom` (optional) sets a per-provider allowlist for command authorization. When configured, it is the
|
||||
only authorization source for commands and directives (channel allowlists/pairing and `commands.useAccessGroups`
|
||||
are ignored). Use `"*"` for a global default; provider-specific keys override it.
|
||||
@@ -71,65 +78,94 @@ They run immediately, are stripped before the model sees the message, and the re
|
||||
|
||||
## Command list
|
||||
|
||||
Text + native (when enabled):
|
||||
Current source-of-truth:
|
||||
|
||||
- `/help`
|
||||
- `/commands`
|
||||
- `/tools [compact|verbose]` (show what the current agent can use right now; `verbose` adds descriptions)
|
||||
- `/skill <name> [input]` (run a skill by name)
|
||||
- `/status` (show current status; includes provider usage/quota for the current model provider when available)
|
||||
- `/tasks` (list background tasks for the current session; shows active and recent task details with agent-local fallback counts)
|
||||
- `/allowlist` (list/add/remove allowlist entries)
|
||||
- `/approve <id> <decision>` (resolve exec approval prompts; use the pending approval message for the available decisions)
|
||||
- `/context [list|detail|json]` (explain “context”; `detail` shows per-file + per-tool + per-skill + system prompt size)
|
||||
- `/btw <question>` (ask an ephemeral side question about the current session without changing future session context; see [/tools/btw](/tools/btw))
|
||||
- `/export-session [path]` (alias: `/export`) (export current session to HTML with full system prompt)
|
||||
- `/whoami` (show your sender id; alias: `/id`)
|
||||
- `/session idle <duration|off>` (manage inactivity auto-unfocus for focused thread bindings)
|
||||
- `/session max-age <duration|off>` (manage hard max-age auto-unfocus for focused thread bindings)
|
||||
- `/subagents list|kill|log|info|send|steer|spawn` (inspect, control, or spawn sub-agent runs for the current session)
|
||||
- `/acp spawn|cancel|steer|close|status|set-mode|set|cwd|permissions|timeout|model|reset-options|doctor|install|sessions` (inspect and control ACP runtime sessions)
|
||||
- `/agents` (list thread-bound agents for this session)
|
||||
- `/focus <target>` (Discord: bind this thread, or a new thread, to a session/subagent target)
|
||||
- `/unfocus` (Discord: remove the current thread binding)
|
||||
- `/kill <id|#|all>` (immediately abort one or all running sub-agents for this session; no confirmation message)
|
||||
- `/steer <id|#> <message>` (steer a running sub-agent immediately: in-run when possible, otherwise abort current work and restart on the steer message)
|
||||
- `/tell <id|#> <message>` (alias for `/steer`)
|
||||
- `/config show|get|set|unset` (persist config to disk, owner-only; requires `commands.config: true`)
|
||||
- `/mcp show|get|set|unset` (manage OpenClaw MCP server config, owner-only; requires `commands.mcp: true`)
|
||||
- `/plugins list|show|get|install|enable|disable` (inspect discovered plugins, install new ones, and toggle enablement; owner-only for writes; requires `commands.plugins: true`)
|
||||
- `/plugin` is an alias for `/plugins`.
|
||||
- `/plugin install <spec>` accepts the same plugin specs as `openclaw plugins install`: local path/archive, npm package, or `clawhub:<pkg>`.
|
||||
- Enable/disable writes still reply with a restart hint. On a watched foreground gateway, OpenClaw may perform that restart automatically right after the write.
|
||||
- `/debug show|set|unset|reset` (runtime overrides, owner-only; requires `commands.debug: true`)
|
||||
- `/usage off|tokens|full|cost` (per-response usage footer or local cost summary)
|
||||
- `/tts off|always|inbound|tagged|status|provider|limit|summary|audio` (control TTS; see [/tts](/tools/tts))
|
||||
- Discord: native command is `/voice` (Discord reserves `/tts`); text `/tts` still works.
|
||||
- `/stop`
|
||||
- `/restart`
|
||||
- `/dock-telegram` (alias: `/dock_telegram`) (switch replies to Telegram)
|
||||
- `/dock-discord` (alias: `/dock_discord`) (switch replies to Discord)
|
||||
- `/dock-slack` (alias: `/dock_slack`) (switch replies to Slack)
|
||||
- `/activation mention|always` (groups only)
|
||||
- `/send on|off|inherit` (owner-only)
|
||||
- `/reset` or `/new [model]` (optional model hint; remainder is passed through)
|
||||
- `/think <off|minimal|low|medium|high|xhigh>` (dynamic choices by model/provider; aliases: `/thinking`, `/t`)
|
||||
- `/fast status|on|off` (omitting the arg shows the current effective fast-mode state)
|
||||
- `/verbose on|full|off` (alias: `/v`)
|
||||
- `/reasoning on|off|stream` (alias: `/reason`; when on, sends a separate message prefixed `Reasoning:`; `stream` = Telegram draft only)
|
||||
- `/elevated on|off|ask|full` (alias: `/elev`; `full` skips exec approvals)
|
||||
- `/exec host=<auto|sandbox|gateway|node> security=<deny|allowlist|full> ask=<off|on-miss|always> node=<id>` (send `/exec` to show current)
|
||||
- `/model <name>` (alias: `/models`; or `/<alias>` from `agents.defaults.models.*.alias`)
|
||||
- `/queue <mode>` (plus options like `debounce:2s cap:25 drop:summarize`; send `/queue` to see current settings)
|
||||
- `/bash <command>` (host-only; alias for `! <command>`; requires `commands.bash: true` + `tools.elevated` allowlists)
|
||||
- `/dreaming [on|off|status|help]` (toggle global dreaming or show status; see [Dreaming](/concepts/dreaming))
|
||||
- core built-ins come from `src/auto-reply/commands-registry.shared.ts`
|
||||
- generated dock commands come from `src/auto-reply/commands-registry.data.ts`
|
||||
- plugin commands come from plugin `registerCommand()` calls
|
||||
- actual availability on your gateway still depends on config flags, channel surface, and installed/enabled plugins
|
||||
|
||||
Text-only:
|
||||
### Core built-in commands
|
||||
|
||||
- `/compact [instructions]` (see [/concepts/compaction](/concepts/compaction))
|
||||
- `! <command>` (host-only; one at a time; use `!poll` + `!stop` for long-running jobs)
|
||||
- `!poll` (check output / status; accepts optional `sessionId`; `/bash poll` also works)
|
||||
- `!stop` (stop the running bash job; accepts optional `sessionId`; `/bash stop` also works)
|
||||
Built-in commands available today:
|
||||
|
||||
- `/new [model]` starts a new session; `/reset` is the reset alias.
|
||||
- `/compact [instructions]` compacts the session context. See [/concepts/compaction](/concepts/compaction).
|
||||
- `/stop` aborts the current run.
|
||||
- `/session idle <duration|off>` and `/session max-age <duration|off>` manage thread-binding expiry.
|
||||
- `/think <off|minimal|low|medium|high|xhigh>` sets the thinking level. Aliases: `/thinking`, `/t`.
|
||||
- `/verbose on|off|full` toggles verbose output. Alias: `/v`.
|
||||
- `/fast [status|on|off]` shows or sets fast mode.
|
||||
- `/reasoning [on|off|stream]` toggles reasoning visibility. Alias: `/reason`.
|
||||
- `/elevated [on|off|ask|full]` toggles elevated mode. Alias: `/elev`.
|
||||
- `/exec host=<auto|sandbox|gateway|node> security=<deny|allowlist|full> ask=<off|on-miss|always> node=<id>` shows or sets exec defaults.
|
||||
- `/model [name|#|status]` shows or sets the model.
|
||||
- `/models [provider] [page] [limit=<n>|size=<n>|all]` lists providers or models for a provider.
|
||||
- `/queue <mode>` manages queue behavior (`steer`, `interrupt`, `followup`, `collect`, `steer-backlog`) plus options like `debounce:2s cap:25 drop:summarize`.
|
||||
- `/help` shows the short help summary.
|
||||
- `/commands` shows the generated command catalog.
|
||||
- `/tools [compact|verbose]` shows what the current agent can use right now.
|
||||
- `/status` shows runtime status, including provider usage/quota when available.
|
||||
- `/tasks` lists active/recent background tasks for the current session.
|
||||
- `/context [list|detail|json]` explains how context is assembled.
|
||||
- `/export-session [path]` exports the current session to HTML. Alias: `/export`.
|
||||
- `/whoami` shows your sender id. Alias: `/id`.
|
||||
- `/skill <name> [input]` runs a skill by name.
|
||||
- `/allowlist [list|add|remove] ...` manages allowlist entries. Text-only.
|
||||
- `/approve <id> <decision>` resolves exec approval prompts.
|
||||
- `/btw <question>` asks a side question without changing future session context. See [/tools/btw](/tools/btw).
|
||||
- `/subagents list|kill|log|info|send|steer|spawn` manages sub-agent runs for the current session.
|
||||
- `/acp spawn|cancel|steer|close|sessions|status|set-mode|set|cwd|permissions|timeout|model|reset-options|doctor|install|help` manages ACP sessions and runtime options.
|
||||
- `/focus <target>` binds the current Discord thread or Telegram topic/conversation to a session target.
|
||||
- `/unfocus` removes the current binding.
|
||||
- `/agents` lists thread-bound agents for the current session.
|
||||
- `/kill <id|#|all>` aborts one or all running sub-agents.
|
||||
- `/steer <id|#> <message>` sends steering to a running sub-agent. Alias: `/tell`.
|
||||
- `/config show|get|set|unset` reads or writes `openclaw.json`. Owner-only. Requires `commands.config: true`.
|
||||
- `/mcp show|get|set|unset` reads or writes OpenClaw-managed MCP server config under `mcp.servers`. Owner-only. Requires `commands.mcp: true`.
|
||||
- `/plugins list|inspect|show|get|install|enable|disable` inspects or mutates plugin state. `/plugin` is an alias. Owner-only for writes. Requires `commands.plugins: true`.
|
||||
- `/debug show|set|unset|reset` manages runtime-only config overrides. Owner-only. Requires `commands.debug: true`.
|
||||
- `/usage off|tokens|full|cost` controls the per-response usage footer or prints a local cost summary.
|
||||
- `/tts on|off|status|provider|limit|summary|audio|help` controls TTS. See [/tools/tts](/tools/tts).
|
||||
- `/restart` restarts OpenClaw when enabled. Default: enabled; set `commands.restart: false` to disable it.
|
||||
- `/activation mention|always` sets group activation mode.
|
||||
- `/send on|off|inherit` sets send policy. Owner-only.
|
||||
- `/bash <command>` runs a host shell command. Text-only. Alias: `! <command>`. Requires `commands.bash: true` plus `tools.elevated` allowlists.
|
||||
- `!poll [sessionId]` checks a background bash job.
|
||||
- `!stop [sessionId]` stops a background bash job.
|
||||
|
||||
### Generated dock commands
|
||||
|
||||
Dock commands are generated from channel plugins with native-command support. Current bundled set:
|
||||
|
||||
- `/dock-discord` (alias: `/dock_discord`)
|
||||
- `/dock-mattermost` (alias: `/dock_mattermost`)
|
||||
- `/dock-slack` (alias: `/dock_slack`)
|
||||
- `/dock-telegram` (alias: `/dock_telegram`)
|
||||
|
||||
### Bundled plugin commands
|
||||
|
||||
Bundled plugins can add more slash commands. Current bundled commands in this repo:
|
||||
|
||||
- `/dreaming [on|off|status|help]` toggles memory dreaming. See [Dreaming](/concepts/dreaming).
|
||||
- `/pair [qr|status|pending|approve|cleanup|notify]` manages device pairing/setup flow. See [Pairing](/channels/pairing).
|
||||
- `/phone status|arm <camera|screen|writes|all> [duration]|disarm` temporarily arms high-risk phone node commands.
|
||||
- `/voice status|list [limit]|set <voiceId|name>` manages Talk voice config. On Discord, the native command name is `/talkvoice`.
|
||||
- `/card ...` sends LINE rich card presets. See [LINE](/channels/line).
|
||||
- QQBot-only commands:
|
||||
- `/bot-ping`
|
||||
- `/bot-version`
|
||||
- `/bot-help`
|
||||
- `/bot-upgrade`
|
||||
- `/bot-logs`
|
||||
|
||||
### Dynamic skill commands
|
||||
|
||||
User-invocable skills are also exposed as slash commands:
|
||||
|
||||
- `/skill <name> [input]` always works as the generic entrypoint.
|
||||
- skills may also appear as direct commands like `/prose` when the skill/plugin registers them.
|
||||
- native skill-command registration is controlled by `commands.nativeSkills` and `channels.<provider>.commands.nativeSkills`.
|
||||
|
||||
Notes:
|
||||
|
||||
@@ -140,6 +176,8 @@ Notes:
|
||||
- In multi-account channels, config-targeted `/allowlist --account <id>` and `/config set channels.<provider>.accounts.<id>...` also honor the target account's `configWrites`.
|
||||
- `/usage` controls the per-response usage footer; `/usage cost` prints a local cost summary from OpenClaw session logs.
|
||||
- `/restart` is enabled by default; set `commands.restart: false` to disable it.
|
||||
- `/plugins install <spec>` accepts the same plugin specs as `openclaw plugins install`: local path/archive, npm package, or `clawhub:<pkg>`.
|
||||
- `/plugins enable|disable` updates plugin config and may prompt for a restart.
|
||||
- Discord-only native command: `/vc join|leave|status` controls voice channels (requires `channels.discord.voice` and native commands; not available as text).
|
||||
- Discord thread-binding commands (`/focus`, `/unfocus`, `/agents`, `/session idle`, `/session max-age`) require effective thread bindings to be enabled (`session.threadBindings.enabled` and/or `channels.discord.threadBindings.enabled`).
|
||||
- ACP command reference and runtime behavior: [ACP Agents](/tools/acp-agents).
|
||||
|
||||
@@ -59,7 +59,7 @@ so that provider must also be authenticated if you enable summaries.
|
||||
## Is it enabled by default?
|
||||
|
||||
No. Auto‑TTS is **off** by default. Enable it in config with
|
||||
`messages.tts.auto` or per session with `/tts always` (alias: `/tts on`).
|
||||
`messages.tts.auto` or locally with `/tts on`.
|
||||
|
||||
When `messages.tts.provider` is unset, OpenClaw picks the first configured
|
||||
speech provider in registry auto-select order.
|
||||
@@ -411,9 +411,7 @@ Discord note: `/tts` is a built-in Discord command, so OpenClaw registers
|
||||
|
||||
```
|
||||
/tts off
|
||||
/tts always
|
||||
/tts inbound
|
||||
/tts tagged
|
||||
/tts on
|
||||
/tts status
|
||||
/tts provider openai
|
||||
/tts limit 2000
|
||||
@@ -425,7 +423,9 @@ Notes:
|
||||
|
||||
- Commands require an authorized sender (allowlist/owner rules still apply).
|
||||
- `commands.text` or native command registration must be enabled.
|
||||
- `off|always|inbound|tagged` are per‑session toggles (`/tts on` is an alias for `/tts always`).
|
||||
- Config `messages.tts.auto` accepts `off|always|inbound|tagged`.
|
||||
- `/tts on` writes the local TTS preference to `always`; `/tts off` writes it to `off`.
|
||||
- Use config when you want `inbound` or `tagged` defaults.
|
||||
- `limit` and `summary` are stored in local prefs, not the main config.
|
||||
- `/tts audio` generates a one-off audio reply (does not toggle TTS on).
|
||||
- `/tts status` includes fallback visibility for the latest attempt:
|
||||
|
||||
@@ -32,7 +32,8 @@
|
||||
},
|
||||
"timeoutSeconds": {
|
||||
"type": "number",
|
||||
"minimum": 0.001
|
||||
"minimum": 0.001,
|
||||
"default": 120
|
||||
},
|
||||
"queueOwnerTtlSeconds": {
|
||||
"type": "number",
|
||||
@@ -106,7 +107,7 @@
|
||||
},
|
||||
"timeoutSeconds": {
|
||||
"label": "Prompt Timeout Seconds",
|
||||
"help": "Optional timeout for each embedded runtime turn.",
|
||||
"help": "Timeout for each embedded runtime turn. Defaults to 120 seconds so slower Gemini CLI ACP startups have room to initialize.",
|
||||
"advanced": true
|
||||
},
|
||||
"queueOwnerTtlSeconds": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/acpx",
|
||||
"version": "2026.4.6",
|
||||
"version": "2026.4.9",
|
||||
"description": "OpenClaw ACP runtime backend",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
|
||||
@@ -8,6 +8,8 @@ export type AcpxPermissionMode = (typeof ACPX_PERMISSION_MODES)[number];
|
||||
export const ACPX_NON_INTERACTIVE_POLICIES = ["deny", "fail"] as const;
|
||||
export type AcpxNonInteractivePermissionPolicy = (typeof ACPX_NON_INTERACTIVE_POLICIES)[number];
|
||||
|
||||
export const DEFAULT_ACPX_TIMEOUT_SECONDS = 120;
|
||||
|
||||
export type McpServerConfig = {
|
||||
command: string;
|
||||
args?: string[];
|
||||
@@ -92,7 +94,7 @@ export const AcpxPluginConfigSchema = z.strictObject({
|
||||
timeoutSeconds: z
|
||||
.number({ error: "timeoutSeconds must be a number >= 0.001" })
|
||||
.min(0.001, { error: "timeoutSeconds must be a number >= 0.001" })
|
||||
.optional(),
|
||||
.default(DEFAULT_ACPX_TIMEOUT_SECONDS),
|
||||
queueOwnerTtlSeconds: z
|
||||
.number({ error: "queueOwnerTtlSeconds must be a number >= 0" })
|
||||
.min(0, { error: "queueOwnerTtlSeconds must be a number >= 0" })
|
||||
|
||||
@@ -15,9 +15,21 @@ describe("embedded acpx plugin config", () => {
|
||||
expect(resolved.stateDir).toBe(path.join(workspaceDir, "state"));
|
||||
expect(resolved.permissionMode).toBe("approve-reads");
|
||||
expect(resolved.nonInteractivePermissions).toBe("fail");
|
||||
expect(resolved.timeoutSeconds).toBe(120);
|
||||
expect(resolved.agents).toEqual({});
|
||||
});
|
||||
|
||||
it("keeps explicit timeoutSeconds config", () => {
|
||||
const resolved = resolveAcpxPluginConfig({
|
||||
rawConfig: {
|
||||
timeoutSeconds: 300,
|
||||
},
|
||||
workspaceDir: "/tmp/openclaw-acpx",
|
||||
});
|
||||
|
||||
expect(resolved.timeoutSeconds).toBe(300);
|
||||
});
|
||||
|
||||
it("accepts agent command overrides", () => {
|
||||
const resolved = resolveAcpxPluginConfig({
|
||||
rawConfig: {
|
||||
@@ -62,6 +74,9 @@ describe("embedded acpx plugin config", () => {
|
||||
properties: expect.objectContaining({
|
||||
cwd: expect.any(Object),
|
||||
stateDir: expect.any(Object),
|
||||
timeoutSeconds: expect.objectContaining({
|
||||
default: 120,
|
||||
}),
|
||||
agents: expect.any(Object),
|
||||
mcpServers: expect.any(Object),
|
||||
}),
|
||||
|
||||
@@ -3,7 +3,7 @@ import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
|
||||
import type { z } from "openclaw/plugin-sdk/zod";
|
||||
import { AcpxPluginConfigSchema } from "./config-schema.js";
|
||||
import { AcpxPluginConfigSchema, DEFAULT_ACPX_TIMEOUT_SECONDS } from "./config-schema.js";
|
||||
import type {
|
||||
AcpxPluginConfig,
|
||||
AcpxPermissionMode,
|
||||
@@ -238,7 +238,7 @@ export function resolveAcpxPluginConfig(params: {
|
||||
pluginToolsMcpBridge,
|
||||
strictWindowsCmdWrapper:
|
||||
normalized.strictWindowsCmdWrapper ?? DEFAULT_STRICT_WINDOWS_CMD_WRAPPER,
|
||||
timeoutSeconds: normalized.timeoutSeconds,
|
||||
timeoutSeconds: normalized.timeoutSeconds ?? DEFAULT_ACPX_TIMEOUT_SECONDS,
|
||||
queueOwnerTtlSeconds: normalized.queueOwnerTtlSeconds ?? DEFAULT_QUEUE_OWNER_TTL_SECONDS,
|
||||
legacyCompatibilityConfig: {
|
||||
strictWindowsCmdWrapper: normalized.strictWindowsCmdWrapper,
|
||||
|
||||
@@ -112,6 +112,36 @@ describe("createAcpxRuntimeService", () => {
|
||||
await service.stop?.(ctx);
|
||||
});
|
||||
|
||||
it("passes the default runtime timeout to the embedded runtime factory", async () => {
|
||||
const workspaceDir = await makeTempDir();
|
||||
const ctx = createServiceContext(workspaceDir);
|
||||
const runtime = {
|
||||
ensureSession: vi.fn(),
|
||||
runTurn: vi.fn(),
|
||||
cancel: vi.fn(),
|
||||
close: vi.fn(),
|
||||
probeAvailability: vi.fn(async () => {}),
|
||||
isHealthy: vi.fn(() => true),
|
||||
doctor: vi.fn(async () => ({ ok: true, message: "ok" })),
|
||||
};
|
||||
const runtimeFactory = vi.fn(() => runtime as never);
|
||||
const service = createAcpxRuntimeService({
|
||||
runtimeFactory,
|
||||
});
|
||||
|
||||
await service.start(ctx);
|
||||
|
||||
expect(runtimeFactory).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
pluginConfig: expect.objectContaining({
|
||||
timeoutSeconds: 120,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
await service.stop?.(ctx);
|
||||
});
|
||||
|
||||
it("warns when legacy compatibility config is explicitly ignored", async () => {
|
||||
const workspaceDir = await makeTempDir();
|
||||
const ctx = createServiceContext(workspaceDir);
|
||||
|
||||
@@ -1,862 +0,0 @@
|
||||
import fs from "node:fs/promises";
|
||||
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry";
|
||||
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
|
||||
import plugin from "./index.js";
|
||||
|
||||
const hoisted = vi.hoisted(() => {
|
||||
const sessionStore: Record<string, Record<string, unknown>> = {
|
||||
"agent:main:main": {
|
||||
sessionId: "s-main",
|
||||
updatedAt: 0,
|
||||
},
|
||||
};
|
||||
return {
|
||||
sessionStore,
|
||||
updateSessionStore: vi.fn(
|
||||
async (_storePath: string, updater: (store: Record<string, unknown>) => void) => {
|
||||
updater(sessionStore);
|
||||
},
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/config-runtime", async () => {
|
||||
const actual = await vi.importActual<typeof import("openclaw/plugin-sdk/config-runtime")>(
|
||||
"openclaw/plugin-sdk/config-runtime",
|
||||
);
|
||||
return {
|
||||
...actual,
|
||||
updateSessionStore: hoisted.updateSessionStore,
|
||||
};
|
||||
});
|
||||
|
||||
describe("active-memory plugin", () => {
|
||||
const hooks: Record<string, Function> = {};
|
||||
const runEmbeddedPiAgent = vi.fn();
|
||||
const api: any = {
|
||||
pluginConfig: {
|
||||
agents: ["main"],
|
||||
logging: true,
|
||||
},
|
||||
config: {},
|
||||
id: "active-memory",
|
||||
name: "Active Memory",
|
||||
logger: { info: vi.fn(), warn: vi.fn(), debug: vi.fn(), error: vi.fn() },
|
||||
runtime: {
|
||||
agent: {
|
||||
runEmbeddedPiAgent,
|
||||
session: {
|
||||
resolveStorePath: vi.fn(() => "/tmp/openclaw-session-store.json"),
|
||||
loadSessionStore: vi.fn(() => hoisted.sessionStore),
|
||||
saveSessionStore: vi.fn(async () => {}),
|
||||
},
|
||||
},
|
||||
},
|
||||
on: vi.fn((hookName: string, handler: Function) => {
|
||||
hooks[hookName] = handler;
|
||||
}),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
api.pluginConfig = {
|
||||
agents: ["main"],
|
||||
logging: true,
|
||||
};
|
||||
hoisted.sessionStore["agent:main:main"] = {
|
||||
sessionId: "s-main",
|
||||
updatedAt: 0,
|
||||
};
|
||||
for (const key of Object.keys(hooks)) {
|
||||
delete hooks[key];
|
||||
}
|
||||
runEmbeddedPiAgent.mockResolvedValue({
|
||||
payloads: [{ text: "- lemon pepper wings\n- blue cheese" }],
|
||||
});
|
||||
plugin.register(api as unknown as OpenClawPluginApi);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("registers a before_prompt_build hook", () => {
|
||||
expect(api.on).toHaveBeenCalledWith("before_prompt_build", expect.any(Function));
|
||||
});
|
||||
|
||||
it("does not run for agents that are not explicitly targeted", async () => {
|
||||
const result = await hooks.before_prompt_build(
|
||||
{ prompt: "what wings should i order?", messages: [] },
|
||||
{
|
||||
agentId: "support",
|
||||
trigger: "user",
|
||||
sessionKey: "agent:support:main",
|
||||
messageProvider: "webchat",
|
||||
},
|
||||
);
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
expect(runEmbeddedPiAgent).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not rewrite session state for skipped turns with no active-memory entry to clear", async () => {
|
||||
const result = await hooks.before_prompt_build(
|
||||
{ prompt: "what wings should i order?", messages: [] },
|
||||
{
|
||||
agentId: "support",
|
||||
trigger: "user",
|
||||
sessionKey: "agent:support:main",
|
||||
messageProvider: "webchat",
|
||||
},
|
||||
);
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
expect(hoisted.updateSessionStore).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not run for non-interactive contexts", async () => {
|
||||
const result = await hooks.before_prompt_build(
|
||||
{ prompt: "what wings should i order?", messages: [] },
|
||||
{
|
||||
agentId: "main",
|
||||
trigger: "heartbeat",
|
||||
sessionKey: "agent:main:main",
|
||||
messageProvider: "webchat",
|
||||
},
|
||||
);
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
expect(runEmbeddedPiAgent).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("defaults to direct-style sessions only", async () => {
|
||||
const result = await hooks.before_prompt_build(
|
||||
{ prompt: "what wings should we order?", messages: [] },
|
||||
{
|
||||
agentId: "main",
|
||||
trigger: "user",
|
||||
sessionKey: "agent:main:telegram:group:-100123",
|
||||
messageProvider: "telegram",
|
||||
channelId: "telegram",
|
||||
},
|
||||
);
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
expect(runEmbeddedPiAgent).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("runs for group sessions when group chat types are explicitly allowed", async () => {
|
||||
api.pluginConfig = {
|
||||
agents: ["main"],
|
||||
allowedChatTypes: ["direct", "group"],
|
||||
};
|
||||
plugin.register(api as unknown as OpenClawPluginApi);
|
||||
|
||||
const result = await hooks.before_prompt_build(
|
||||
{ prompt: "what wings should we order?", messages: [] },
|
||||
{
|
||||
agentId: "main",
|
||||
trigger: "user",
|
||||
sessionKey: "agent:main:telegram:group:-100123",
|
||||
messageProvider: "telegram",
|
||||
channelId: "telegram",
|
||||
},
|
||||
);
|
||||
|
||||
expect(runEmbeddedPiAgent).toHaveBeenCalledTimes(1);
|
||||
expect(result).toEqual({
|
||||
prependSystemContext: expect.stringContaining("plugin-provided supplemental context"),
|
||||
appendSystemContext: expect.stringContaining("<active_memory_plugin>"),
|
||||
});
|
||||
});
|
||||
|
||||
it("injects system context on a successful recall hit", async () => {
|
||||
const result = await hooks.before_prompt_build(
|
||||
{
|
||||
prompt: "what wings should i order?",
|
||||
messages: [
|
||||
{ role: "user", content: "i want something greasy tonight" },
|
||||
{ role: "assistant", content: "let's narrow it down" },
|
||||
],
|
||||
},
|
||||
{
|
||||
agentId: "main",
|
||||
trigger: "user",
|
||||
sessionKey: "agent:main:main",
|
||||
messageProvider: "webchat",
|
||||
},
|
||||
);
|
||||
|
||||
expect(runEmbeddedPiAgent).toHaveBeenCalledTimes(1);
|
||||
expect(result).toEqual({
|
||||
prependSystemContext: expect.stringContaining("plugin-provided supplemental context"),
|
||||
appendSystemContext: expect.stringContaining("<active_memory_plugin>"),
|
||||
});
|
||||
expect((result as { appendSystemContext: string }).appendSystemContext).toContain(
|
||||
"lemon pepper wings",
|
||||
);
|
||||
expect(runEmbeddedPiAgent.mock.calls.at(-1)?.[0]).toMatchObject({
|
||||
provider: "github-copilot",
|
||||
model: "gpt-5.4-mini",
|
||||
sessionKey: expect.stringMatching(/^agent:main:main:active-memory:[a-f0-9]{12}$/),
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves leading digits in recalled memory bullets", async () => {
|
||||
runEmbeddedPiAgent.mockResolvedValueOnce({
|
||||
payloads: [{ text: "- 2024 trip to tokyo\n- 2% milk" }],
|
||||
});
|
||||
|
||||
const result = await hooks.before_prompt_build(
|
||||
{
|
||||
prompt: "what should i remember from my 2024 trip and should i buy 2% milk?",
|
||||
messages: [],
|
||||
},
|
||||
{
|
||||
agentId: "main",
|
||||
trigger: "user",
|
||||
sessionKey: "agent:main:main",
|
||||
messageProvider: "webchat",
|
||||
},
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
prependSystemContext: expect.stringContaining("plugin-provided supplemental context"),
|
||||
appendSystemContext: expect.stringContaining("<active_memory_plugin>"),
|
||||
});
|
||||
expect((result as { appendSystemContext: string }).appendSystemContext).toContain(
|
||||
"2024 trip to tokyo",
|
||||
);
|
||||
expect((result as { appendSystemContext: string }).appendSystemContext).toContain("2% milk");
|
||||
});
|
||||
|
||||
it("preserves canonical parent session scope in the blocking memory subagent session key", async () => {
|
||||
await hooks.before_prompt_build(
|
||||
{ prompt: "what should i grab on the way?", messages: [] },
|
||||
{
|
||||
agentId: "main",
|
||||
trigger: "user",
|
||||
sessionKey: "agent:main:telegram:direct:12345:thread:99",
|
||||
messageProvider: "telegram",
|
||||
channelId: "telegram",
|
||||
},
|
||||
);
|
||||
|
||||
expect(runEmbeddedPiAgent.mock.calls.at(-1)?.[0]?.sessionKey).toMatch(
|
||||
/^agent:main:telegram:direct:12345:thread:99:active-memory:[a-f0-9]{12}$/,
|
||||
);
|
||||
});
|
||||
|
||||
it("falls back to the current session model when no plugin model is configured", async () => {
|
||||
api.pluginConfig = {
|
||||
agents: ["main"],
|
||||
};
|
||||
plugin.register(api as unknown as OpenClawPluginApi);
|
||||
|
||||
await hooks.before_prompt_build(
|
||||
{ prompt: "what wings should i order? temp transcript", messages: [] },
|
||||
{
|
||||
agentId: "main",
|
||||
trigger: "user",
|
||||
sessionKey: "agent:main:main",
|
||||
messageProvider: "webchat",
|
||||
modelProviderId: "qwen",
|
||||
modelId: "glm-5",
|
||||
},
|
||||
);
|
||||
|
||||
expect(runEmbeddedPiAgent.mock.calls.at(-1)?.[0]).toMatchObject({
|
||||
provider: "qwen",
|
||||
model: "glm-5",
|
||||
});
|
||||
});
|
||||
|
||||
it("can disable default remote model fallback", async () => {
|
||||
api.pluginConfig = {
|
||||
agents: ["main"],
|
||||
modelFallbackPolicy: "resolved-only",
|
||||
};
|
||||
plugin.register(api as unknown as OpenClawPluginApi);
|
||||
|
||||
const result = await hooks.before_prompt_build(
|
||||
{ prompt: "what wings should i order? no fallback", messages: [] },
|
||||
{
|
||||
agentId: "main",
|
||||
trigger: "user",
|
||||
sessionKey: "agent:main:resolved-only",
|
||||
messageProvider: "webchat",
|
||||
},
|
||||
);
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
expect(runEmbeddedPiAgent).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("persists a readable debug summary alongside the status line", async () => {
|
||||
const sessionKey = "agent:main:debug";
|
||||
hoisted.sessionStore[sessionKey] = {
|
||||
sessionId: "s-main",
|
||||
updatedAt: 0,
|
||||
};
|
||||
|
||||
await hooks.before_prompt_build(
|
||||
{
|
||||
prompt: "what wings should i order?",
|
||||
messages: [],
|
||||
},
|
||||
{ agentId: "main", trigger: "user", sessionKey, messageProvider: "webchat" },
|
||||
);
|
||||
|
||||
expect(hoisted.updateSessionStore).toHaveBeenCalled();
|
||||
const updater = hoisted.updateSessionStore.mock.calls.at(-1)?.[1] as
|
||||
| ((store: Record<string, Record<string, unknown>>) => void)
|
||||
| undefined;
|
||||
const store = {
|
||||
[sessionKey]: {
|
||||
sessionId: "s-main",
|
||||
updatedAt: 0,
|
||||
},
|
||||
} as Record<string, Record<string, unknown>>;
|
||||
updater?.(store);
|
||||
expect(store[sessionKey]?.pluginDebugEntries).toEqual([
|
||||
{
|
||||
pluginId: "active-memory",
|
||||
lines: expect.arrayContaining([
|
||||
expect.stringContaining("🧩 Active Memory: ok"),
|
||||
expect.stringContaining("🔎 Active Memory Debug: lemon pepper wings"),
|
||||
]),
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("replaces stale legacy active-memory lines on a later empty run", async () => {
|
||||
const sessionKey = "agent:main:legacy-active-memory-lines";
|
||||
hoisted.sessionStore[sessionKey] = {
|
||||
sessionId: "s-main",
|
||||
updatedAt: 0,
|
||||
pluginStatusLines: [
|
||||
"Active Memory: ok 13.4s recent 1 mem",
|
||||
"Active Memory Debug: Favorite desk snack: roasted almonds or cashews.",
|
||||
"Other Plugin: keep me",
|
||||
],
|
||||
};
|
||||
runEmbeddedPiAgent.mockResolvedValueOnce({
|
||||
payloads: [{ text: "NONE" }],
|
||||
});
|
||||
|
||||
await hooks.before_prompt_build(
|
||||
{ prompt: "what's up with you?", messages: [] },
|
||||
{ agentId: "main", trigger: "user", sessionKey, messageProvider: "webchat" },
|
||||
);
|
||||
|
||||
const updater = hoisted.updateSessionStore.mock.calls.at(-1)?.[1] as
|
||||
| ((store: Record<string, Record<string, unknown>>) => void)
|
||||
| undefined;
|
||||
const store = {
|
||||
[sessionKey]: {
|
||||
sessionId: "s-main",
|
||||
updatedAt: 0,
|
||||
pluginStatusLines: [
|
||||
"Active Memory: ok 13.4s recent 1 mem",
|
||||
"Active Memory Debug: Favorite desk snack: roasted almonds or cashews.",
|
||||
"Other Plugin: keep me",
|
||||
],
|
||||
},
|
||||
} as Record<string, Record<string, unknown>>;
|
||||
updater?.(store);
|
||||
|
||||
expect(store[sessionKey]?.pluginDebugEntries).toEqual([
|
||||
{
|
||||
pluginId: "active-memory",
|
||||
lines: [expect.stringContaining("🧩 Active Memory: empty")],
|
||||
},
|
||||
]);
|
||||
expect(store[sessionKey]?.pluginStatusLines).toEqual(["Other Plugin: keep me"]);
|
||||
});
|
||||
|
||||
it("returns nothing when the sidecar says none", async () => {
|
||||
runEmbeddedPiAgent.mockResolvedValueOnce({
|
||||
payloads: [{ text: "NONE" }],
|
||||
});
|
||||
|
||||
const result = await hooks.before_prompt_build(
|
||||
{ prompt: "fair, okay gonna do them by throwing them in the garbage", messages: [] },
|
||||
{
|
||||
agentId: "main",
|
||||
trigger: "user",
|
||||
sessionKey: "agent:main:main",
|
||||
messageProvider: "webchat",
|
||||
},
|
||||
);
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it("does not cache timeout results", async () => {
|
||||
api.pluginConfig = {
|
||||
agents: ["main"],
|
||||
timeoutMs: 250,
|
||||
logging: true,
|
||||
};
|
||||
plugin.register(api as unknown as OpenClawPluginApi);
|
||||
let lastAbortSignal: AbortSignal | undefined;
|
||||
runEmbeddedPiAgent.mockImplementation(async (params: { abortSignal?: AbortSignal }) => {
|
||||
lastAbortSignal = params.abortSignal;
|
||||
return await new Promise((resolve, reject) => {
|
||||
const abortHandler = () => reject(new Error("aborted"));
|
||||
params.abortSignal?.addEventListener("abort", abortHandler, { once: true });
|
||||
setTimeout(() => {
|
||||
params.abortSignal?.removeEventListener("abort", abortHandler);
|
||||
resolve({ payloads: [] });
|
||||
}, 2_000);
|
||||
});
|
||||
});
|
||||
|
||||
await hooks.before_prompt_build(
|
||||
{ prompt: "what wings should i order? timeout test", messages: [] },
|
||||
{
|
||||
agentId: "main",
|
||||
trigger: "user",
|
||||
sessionKey: "agent:main:timeout-test",
|
||||
messageProvider: "webchat",
|
||||
},
|
||||
);
|
||||
await hooks.before_prompt_build(
|
||||
{ prompt: "what wings should i order? timeout test", messages: [] },
|
||||
{
|
||||
agentId: "main",
|
||||
trigger: "user",
|
||||
sessionKey: "agent:main:timeout-test",
|
||||
messageProvider: "webchat",
|
||||
},
|
||||
);
|
||||
|
||||
expect(hoisted.updateSessionStore).toHaveBeenCalledTimes(2);
|
||||
expect(lastAbortSignal?.aborted).toBe(true);
|
||||
const infoLines = vi
|
||||
.mocked(api.logger.info)
|
||||
.mock.calls.map((call: unknown[]) => String(call[0]));
|
||||
expect(infoLines.some((line: string) => line.includes(" cached "))).toBe(false);
|
||||
});
|
||||
|
||||
it("does not share cached recall results across session-id-only contexts", async () => {
|
||||
api.pluginConfig = {
|
||||
agents: ["main"],
|
||||
logging: true,
|
||||
};
|
||||
plugin.register(api as unknown as OpenClawPluginApi);
|
||||
|
||||
await hooks.before_prompt_build(
|
||||
{ prompt: "what wings should i order? session id cache", messages: [] },
|
||||
{
|
||||
agentId: "main",
|
||||
trigger: "user",
|
||||
sessionId: "session-a",
|
||||
messageProvider: "webchat",
|
||||
},
|
||||
);
|
||||
await hooks.before_prompt_build(
|
||||
{ prompt: "what wings should i order? session id cache", messages: [] },
|
||||
{
|
||||
agentId: "main",
|
||||
trigger: "user",
|
||||
sessionId: "session-b",
|
||||
messageProvider: "webchat",
|
||||
},
|
||||
);
|
||||
|
||||
expect(runEmbeddedPiAgent).toHaveBeenCalledTimes(2);
|
||||
const infoLines = vi
|
||||
.mocked(api.logger.info)
|
||||
.mock.calls.map((call: unknown[]) => String(call[0]));
|
||||
expect(infoLines.some((line: string) => line.includes(" cached "))).toBe(false);
|
||||
});
|
||||
|
||||
it("uses a canonical agent session key when only sessionId is available", async () => {
|
||||
hoisted.sessionStore["agent:main:telegram:direct:12345"] = {
|
||||
sessionId: "session-a",
|
||||
updatedAt: 25,
|
||||
};
|
||||
|
||||
await hooks.before_prompt_build(
|
||||
{ prompt: "what wings should i order? session id only", messages: [] },
|
||||
{
|
||||
agentId: "main",
|
||||
trigger: "user",
|
||||
sessionId: "session-a",
|
||||
messageProvider: "webchat",
|
||||
},
|
||||
);
|
||||
|
||||
expect(runEmbeddedPiAgent.mock.calls.at(-1)?.[0]?.sessionKey).toMatch(
|
||||
/^agent:main:telegram:direct:12345:active-memory:[a-f0-9]{12}$/,
|
||||
);
|
||||
});
|
||||
|
||||
it("clears stale status on skipped non-interactive turns even when agentId is missing", async () => {
|
||||
const sessionKey = "agent:main:missing-agent";
|
||||
hoisted.sessionStore[sessionKey] = {
|
||||
sessionId: "s-main",
|
||||
updatedAt: 0,
|
||||
pluginDebugEntries: [
|
||||
{ pluginId: "active-memory", lines: ["🧩 Active Memory: timeout 15s recent"] },
|
||||
],
|
||||
};
|
||||
|
||||
const result = await hooks.before_prompt_build(
|
||||
{ prompt: "what wings should i order?", messages: [] },
|
||||
{ trigger: "heartbeat", sessionKey, messageProvider: "webchat" },
|
||||
);
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
const updater = hoisted.updateSessionStore.mock.calls.at(-1)?.[1] as
|
||||
| ((store: Record<string, Record<string, unknown>>) => void)
|
||||
| undefined;
|
||||
const store = {
|
||||
[sessionKey]: {
|
||||
sessionId: "s-main",
|
||||
updatedAt: 0,
|
||||
pluginDebugEntries: [
|
||||
{ pluginId: "active-memory", lines: ["🧩 Active Memory: timeout 15s recent"] },
|
||||
],
|
||||
},
|
||||
} as Record<string, Record<string, unknown>>;
|
||||
updater?.(store);
|
||||
expect(store[sessionKey]?.pluginDebugEntries).toBeUndefined();
|
||||
});
|
||||
|
||||
it("supports message mode by sending only the latest user message", async () => {
|
||||
api.pluginConfig = {
|
||||
agents: ["main"],
|
||||
queryMode: "message",
|
||||
};
|
||||
plugin.register(api as unknown as OpenClawPluginApi);
|
||||
|
||||
await hooks.before_prompt_build(
|
||||
{
|
||||
prompt: "what should i grab on the way?",
|
||||
messages: [
|
||||
{ role: "user", content: "i have a flight tomorrow" },
|
||||
{ role: "assistant", content: "got it" },
|
||||
],
|
||||
},
|
||||
{
|
||||
agentId: "main",
|
||||
trigger: "user",
|
||||
sessionKey: "agent:main:main",
|
||||
messageProvider: "webchat",
|
||||
},
|
||||
);
|
||||
|
||||
const prompt = runEmbeddedPiAgent.mock.calls.at(-1)?.[0]?.prompt;
|
||||
expect(prompt).toContain("Conversation context:\nwhat should i grab on the way?");
|
||||
expect(prompt).not.toContain("Recent conversation tail:");
|
||||
});
|
||||
|
||||
it("supports full mode by sending the whole conversation", async () => {
|
||||
api.pluginConfig = {
|
||||
agents: ["main"],
|
||||
queryMode: "full",
|
||||
};
|
||||
plugin.register(api as unknown as OpenClawPluginApi);
|
||||
|
||||
await hooks.before_prompt_build(
|
||||
{
|
||||
prompt: "what should i grab on the way?",
|
||||
messages: [
|
||||
{ role: "user", content: "i have a flight tomorrow" },
|
||||
{ role: "assistant", content: "got it" },
|
||||
{ role: "user", content: "packing is annoying" },
|
||||
],
|
||||
},
|
||||
{
|
||||
agentId: "main",
|
||||
trigger: "user",
|
||||
sessionKey: "agent:main:main",
|
||||
messageProvider: "webchat",
|
||||
},
|
||||
);
|
||||
|
||||
const prompt = runEmbeddedPiAgent.mock.calls.at(-1)?.[0]?.prompt;
|
||||
expect(prompt).toContain("Full conversation context:");
|
||||
expect(prompt).toContain("user: i have a flight tomorrow");
|
||||
expect(prompt).toContain("assistant: got it");
|
||||
expect(prompt).toContain("user: packing is annoying");
|
||||
});
|
||||
|
||||
it("strips prior memory/debug traces from assistant context before retrieval", async () => {
|
||||
api.pluginConfig = {
|
||||
agents: ["main"],
|
||||
queryMode: "recent",
|
||||
};
|
||||
plugin.register(api as unknown as OpenClawPluginApi);
|
||||
|
||||
await hooks.before_prompt_build(
|
||||
{
|
||||
prompt: "what should i grab on the way?",
|
||||
messages: [
|
||||
{ role: "user", content: "i have a flight tomorrow" },
|
||||
{
|
||||
role: "assistant",
|
||||
content:
|
||||
"🧠 Memory Search: favorite food comfort food tacos sushi ramen\n🧩 Active Memory: ok 842ms recent 2 mem\n🔎 Active Memory Debug: spicy ramen; tacos\nSounds like you want something easy before the airport.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
agentId: "main",
|
||||
trigger: "user",
|
||||
sessionKey: "agent:main:main",
|
||||
messageProvider: "webchat",
|
||||
},
|
||||
);
|
||||
|
||||
const prompt = runEmbeddedPiAgent.mock.calls.at(-1)?.[0]?.prompt;
|
||||
expect(prompt).toContain(
|
||||
"ignore that text and do not search for those same surfaced memories again",
|
||||
);
|
||||
expect(prompt).toContain("assistant: Sounds like you want something easy before the airport.");
|
||||
expect(prompt).not.toContain("Memory Search:");
|
||||
expect(prompt).not.toContain("Active Memory:");
|
||||
expect(prompt).not.toContain("Active Memory Debug:");
|
||||
expect(prompt).not.toContain("spicy ramen; tacos");
|
||||
});
|
||||
|
||||
it("trusts the subagent's relevance decision for explicit preference recall prompts", async () => {
|
||||
runEmbeddedPiAgent.mockResolvedValueOnce({
|
||||
payloads: [{ text: "- aisle seat\n- extra buffer on connections" }],
|
||||
});
|
||||
|
||||
const result = await hooks.before_prompt_build(
|
||||
{ prompt: "u remember my flight preferences", messages: [] },
|
||||
{
|
||||
agentId: "main",
|
||||
trigger: "user",
|
||||
sessionKey: "agent:main:main",
|
||||
messageProvider: "webchat",
|
||||
},
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
prependSystemContext: expect.stringContaining("plugin-provided supplemental context"),
|
||||
appendSystemContext: expect.stringContaining("aisle seat"),
|
||||
});
|
||||
expect((result as { appendSystemContext: string }).appendSystemContext).toContain(
|
||||
"extra buffer on connections",
|
||||
);
|
||||
});
|
||||
|
||||
it("applies total summary truncation after normalizing the sidecar reply", async () => {
|
||||
api.pluginConfig = {
|
||||
agents: ["main"],
|
||||
maxSummaryChars: 40,
|
||||
};
|
||||
plugin.register(api as unknown as OpenClawPluginApi);
|
||||
runEmbeddedPiAgent.mockResolvedValueOnce({
|
||||
payloads: [
|
||||
{
|
||||
text: "- lemon pepper wings with extra crisp skin\n- blue cheese dressing on the side",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const result = await hooks.before_prompt_build(
|
||||
{ prompt: "what wings should i order?", messages: [] },
|
||||
{
|
||||
agentId: "main",
|
||||
trigger: "user",
|
||||
sessionKey: "agent:main:main",
|
||||
messageProvider: "webchat",
|
||||
},
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
prependSystemContext: expect.stringContaining("plugin-provided supplemental context"),
|
||||
appendSystemContext: expect.stringContaining("lemon pepper wings"),
|
||||
});
|
||||
expect((result as { appendSystemContext: string }).appendSystemContext).not.toContain(
|
||||
"dressing on the side",
|
||||
);
|
||||
});
|
||||
|
||||
it("uses the configured maxSummaryChars value in the sidecar prompt", async () => {
|
||||
api.pluginConfig = {
|
||||
agents: ["main"],
|
||||
maxSummaryChars: 90,
|
||||
};
|
||||
plugin.register(api as unknown as OpenClawPluginApi);
|
||||
|
||||
await hooks.before_prompt_build(
|
||||
{ prompt: "what wings should i order? prompt-count-check", messages: [] },
|
||||
{
|
||||
agentId: "main",
|
||||
trigger: "user",
|
||||
sessionKey: "agent:main:prompt-count-check",
|
||||
messageProvider: "webchat",
|
||||
},
|
||||
);
|
||||
|
||||
expect(runEmbeddedPiAgent.mock.calls.at(-1)?.[0]?.prompt).toContain(
|
||||
"If something is useful, reply with one compact active-memory summary under 90 characters total.",
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps sidecar transcripts off disk by default by using a temp session file", async () => {
|
||||
const mkdtempSpy = vi
|
||||
.spyOn(fs, "mkdtemp")
|
||||
.mockResolvedValue("/tmp/openclaw-active-memory-temp");
|
||||
const rmSpy = vi.spyOn(fs, "rm").mockResolvedValue(undefined);
|
||||
|
||||
await hooks.before_prompt_build(
|
||||
{ prompt: "what wings should i order? temp transcript path", messages: [] },
|
||||
{
|
||||
agentId: "main",
|
||||
trigger: "user",
|
||||
sessionKey: "agent:main:main",
|
||||
messageProvider: "webchat",
|
||||
},
|
||||
);
|
||||
|
||||
expect(mkdtempSpy).toHaveBeenCalled();
|
||||
expect(runEmbeddedPiAgent.mock.calls.at(-1)?.[0]?.sessionFile).toBe(
|
||||
"/tmp/openclaw-active-memory-temp/session.jsonl",
|
||||
);
|
||||
expect(rmSpy).toHaveBeenCalledWith("/tmp/openclaw-active-memory-temp", {
|
||||
recursive: true,
|
||||
force: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("persists sidecar transcripts in a separate directory when enabled", async () => {
|
||||
api.pluginConfig = {
|
||||
agents: ["main"],
|
||||
persistTranscripts: true,
|
||||
transcriptDir: "active-memory-sidecars",
|
||||
logging: true,
|
||||
};
|
||||
plugin.register(api as unknown as OpenClawPluginApi);
|
||||
const mkdirSpy = vi.spyOn(fs, "mkdir").mockResolvedValue(undefined);
|
||||
const mkdtempSpy = vi.spyOn(fs, "mkdtemp");
|
||||
const rmSpy = vi.spyOn(fs, "rm").mockResolvedValue(undefined);
|
||||
|
||||
const sessionKey = "agent:main:persist-transcript";
|
||||
await hooks.before_prompt_build(
|
||||
{ prompt: "what wings should i order? persist transcript", messages: [] },
|
||||
{ agentId: "main", trigger: "user", sessionKey, messageProvider: "webchat" },
|
||||
);
|
||||
|
||||
expect(mkdirSpy).toHaveBeenCalledWith("/tmp/active-memory-sidecars", { recursive: true });
|
||||
expect(mkdtempSpy).not.toHaveBeenCalled();
|
||||
expect(runEmbeddedPiAgent.mock.calls.at(-1)?.[0]?.sessionFile).toMatch(
|
||||
/^\/tmp\/active-memory-sidecars\/active-memory-[a-z0-9]+-[a-f0-9]{8}\.jsonl$/,
|
||||
);
|
||||
expect(rmSpy).not.toHaveBeenCalled();
|
||||
expect(
|
||||
vi
|
||||
.mocked(api.logger.info)
|
||||
.mock.calls.some((call: unknown[]) =>
|
||||
String(call[0]).includes("transcript=/tmp/active-memory-sidecars/"),
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("falls back to the default transcript directory when transcriptDir is unsafe", async () => {
|
||||
api.pluginConfig = {
|
||||
agents: ["main"],
|
||||
persistTranscripts: true,
|
||||
transcriptDir: "C:/temp/escape",
|
||||
logging: true,
|
||||
};
|
||||
plugin.register(api as unknown as OpenClawPluginApi);
|
||||
const mkdirSpy = vi.spyOn(fs, "mkdir").mockResolvedValue(undefined);
|
||||
|
||||
await hooks.before_prompt_build(
|
||||
{ prompt: "what wings should i order? unsafe transcript dir", messages: [] },
|
||||
{
|
||||
agentId: "main",
|
||||
trigger: "user",
|
||||
sessionKey: "agent:main:unsafe-transcript",
|
||||
messageProvider: "webchat",
|
||||
},
|
||||
);
|
||||
|
||||
expect(mkdirSpy).toHaveBeenCalledWith("/tmp/active-memory", { recursive: true });
|
||||
expect(runEmbeddedPiAgent.mock.calls.at(-1)?.[0]?.sessionFile).toMatch(
|
||||
/^\/tmp\/active-memory\/active-memory-[a-z0-9]+-[a-f0-9]{8}\.jsonl$/,
|
||||
);
|
||||
});
|
||||
|
||||
it("sanitizes control characters out of debug lines", async () => {
|
||||
const sessionKey = "agent:main:debug-sanitize";
|
||||
hoisted.sessionStore[sessionKey] = {
|
||||
sessionId: "s-main",
|
||||
updatedAt: 0,
|
||||
};
|
||||
runEmbeddedPiAgent.mockResolvedValueOnce({
|
||||
payloads: [{ text: "- spicy ramen\u001b[31m\n- fries\r\n- blue cheese\t" }],
|
||||
});
|
||||
|
||||
await hooks.before_prompt_build(
|
||||
{ prompt: "what should i order?", messages: [] },
|
||||
{ agentId: "main", trigger: "user", sessionKey, messageProvider: "webchat" },
|
||||
);
|
||||
|
||||
const updater = hoisted.updateSessionStore.mock.calls.at(-1)?.[1] as
|
||||
| ((store: Record<string, Record<string, unknown>>) => void)
|
||||
| undefined;
|
||||
const store = {
|
||||
[sessionKey]: {
|
||||
sessionId: "s-main",
|
||||
updatedAt: 0,
|
||||
},
|
||||
} as Record<string, Record<string, unknown>>;
|
||||
updater?.(store);
|
||||
const lines =
|
||||
(store[sessionKey]?.pluginDebugEntries as Array<{ lines?: string[] }> | undefined)?.[0]
|
||||
?.lines ?? [];
|
||||
expect(lines.some((line) => line.includes("\u001b"))).toBe(false);
|
||||
expect(lines.some((line) => line.includes("\r"))).toBe(false);
|
||||
});
|
||||
|
||||
it("caps the active-memory cache size and evicts the oldest entries", async () => {
|
||||
api.pluginConfig = {
|
||||
agents: ["main"],
|
||||
logging: true,
|
||||
};
|
||||
plugin.register(api as unknown as OpenClawPluginApi);
|
||||
|
||||
for (let index = 0; index <= 1000; index += 1) {
|
||||
await hooks.before_prompt_build(
|
||||
{ prompt: `cache pressure prompt ${index}`, messages: [] },
|
||||
{
|
||||
agentId: "main",
|
||||
trigger: "user",
|
||||
sessionKey: "agent:main:cache-cap",
|
||||
messageProvider: "webchat",
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const callsBeforeReplay = runEmbeddedPiAgent.mock.calls.length;
|
||||
|
||||
await hooks.before_prompt_build(
|
||||
{ prompt: "cache pressure prompt 0", messages: [] },
|
||||
{
|
||||
agentId: "main",
|
||||
trigger: "user",
|
||||
sessionKey: "agent:main:cache-cap",
|
||||
messageProvider: "webchat",
|
||||
},
|
||||
);
|
||||
|
||||
expect(runEmbeddedPiAgent.mock.calls.length).toBe(callsBeforeReplay + 1);
|
||||
const infoLines = vi
|
||||
.mocked(api.logger.info)
|
||||
.mock.calls.map((call: unknown[]) => String(call[0]));
|
||||
expect(
|
||||
infoLines.some(
|
||||
(line: string) => line.includes("cached status=ok") && line.includes("prompt 0"),
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,82 +0,0 @@
|
||||
{
|
||||
"id": "active-memory",
|
||||
"name": "Active Memory",
|
||||
"description": "Runs a bounded blocking memory subagent before eligible conversational replies and injects relevant memory into prompt context.",
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"agents": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" }
|
||||
},
|
||||
"model": { "type": "string" },
|
||||
"modelFallbackPolicy": {
|
||||
"type": "string",
|
||||
"enum": ["default-remote", "resolved-only"]
|
||||
},
|
||||
"allowedChatTypes": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"enum": ["direct", "group", "channel"]
|
||||
}
|
||||
},
|
||||
"timeoutMs": { "type": "integer", "minimum": 250 },
|
||||
"queryMode": {
|
||||
"type": "string",
|
||||
"enum": ["message", "recent", "full"]
|
||||
},
|
||||
"maxSummaryChars": { "type": "integer", "minimum": 40, "maximum": 1000 },
|
||||
"recentUserTurns": { "type": "integer", "minimum": 0, "maximum": 4 },
|
||||
"recentAssistantTurns": { "type": "integer", "minimum": 0, "maximum": 3 },
|
||||
"recentUserChars": { "type": "integer", "minimum": 40, "maximum": 1000 },
|
||||
"recentAssistantChars": { "type": "integer", "minimum": 40, "maximum": 1000 },
|
||||
"logging": { "type": "boolean" },
|
||||
"persistTranscripts": { "type": "boolean" },
|
||||
"transcriptDir": { "type": "string" },
|
||||
"cacheTtlMs": { "type": "integer", "minimum": 1000, "maximum": 120000 }
|
||||
}
|
||||
},
|
||||
"uiHints": {
|
||||
"agents": {
|
||||
"label": "Target Agents",
|
||||
"help": "Explicit agent ids that may use active memory."
|
||||
},
|
||||
"model": {
|
||||
"label": "Memory Model",
|
||||
"help": "Provider/model used for the blocking memory subagent."
|
||||
},
|
||||
"modelFallbackPolicy": {
|
||||
"label": "Model Fallback Policy",
|
||||
"help": "Choose whether Active Memory falls back to the built-in remote default model when no explicit or inherited model is available."
|
||||
},
|
||||
"allowedChatTypes": {
|
||||
"label": "Allowed Chat Types",
|
||||
"help": "Choose which session types may run Active Memory. Defaults to direct-message style sessions only."
|
||||
},
|
||||
"timeoutMs": {
|
||||
"label": "Timeout (ms)"
|
||||
},
|
||||
"queryMode": {
|
||||
"label": "Query Mode",
|
||||
"help": "Choose whether the blocking memory subagent sees only the latest user message, a small recent tail, or the full conversation."
|
||||
},
|
||||
"maxSummaryChars": {
|
||||
"label": "Max Summary Characters",
|
||||
"help": "Maximum total characters allowed in the active-memory summary."
|
||||
},
|
||||
"logging": {
|
||||
"label": "Enable Logging",
|
||||
"help": "Emit active memory timing and result logs."
|
||||
},
|
||||
"persistTranscripts": {
|
||||
"label": "Persist Transcripts",
|
||||
"help": "Keep blocking memory subagent session transcripts on disk in a separate plugin-owned directory."
|
||||
},
|
||||
"transcriptDir": {
|
||||
"label": "Transcript Directory",
|
||||
"help": "Relative directory under the agent sessions folder used when transcript persistence is enabled."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/alibaba-provider",
|
||||
"version": "2026.4.6",
|
||||
"version": "2026.4.9",
|
||||
"private": true,
|
||||
"description": "OpenClaw Alibaba Model Studio video provider plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,71 +1,27 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { buildAlibabaVideoGenerationProvider } from "./video-generation-provider.js";
|
||||
import { beforeAll, describe, expect, it } from "vitest";
|
||||
import {
|
||||
expectDashscopeVideoTaskPoll,
|
||||
expectSuccessfulDashscopeVideoResult,
|
||||
mockSuccessfulDashscopeVideoTask,
|
||||
} from "../../test/helpers/media-generation/dashscope-video-provider.js";
|
||||
import {
|
||||
getProviderHttpMocks,
|
||||
installProviderHttpMockCleanup,
|
||||
} from "../../test/helpers/media-generation/provider-http-mocks.js";
|
||||
|
||||
const {
|
||||
resolveApiKeyForProviderMock,
|
||||
postJsonRequestMock,
|
||||
fetchWithTimeoutMock,
|
||||
assertOkOrThrowHttpErrorMock,
|
||||
resolveProviderHttpRequestConfigMock,
|
||||
} = vi.hoisted(() => ({
|
||||
resolveApiKeyForProviderMock: vi.fn(async () => ({ apiKey: "alibaba-key" })),
|
||||
postJsonRequestMock: vi.fn(),
|
||||
fetchWithTimeoutMock: vi.fn(),
|
||||
assertOkOrThrowHttpErrorMock: vi.fn(async () => {}),
|
||||
resolveProviderHttpRequestConfigMock: vi.fn((params) => ({
|
||||
baseUrl: params.baseUrl ?? params.defaultBaseUrl,
|
||||
allowPrivateNetwork: false,
|
||||
headers: new Headers(params.defaultHeaders),
|
||||
dispatcherPolicy: undefined,
|
||||
})),
|
||||
}));
|
||||
const { postJsonRequestMock, fetchWithTimeoutMock } = getProviderHttpMocks();
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/provider-auth-runtime", () => ({
|
||||
resolveApiKeyForProvider: resolveApiKeyForProviderMock,
|
||||
}));
|
||||
let buildAlibabaVideoGenerationProvider: typeof import("./video-generation-provider.js").buildAlibabaVideoGenerationProvider;
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/provider-http", () => ({
|
||||
assertOkOrThrowHttpError: assertOkOrThrowHttpErrorMock,
|
||||
fetchWithTimeout: fetchWithTimeoutMock,
|
||||
postJsonRequest: postJsonRequestMock,
|
||||
resolveProviderHttpRequestConfig: resolveProviderHttpRequestConfigMock,
|
||||
}));
|
||||
beforeAll(async () => {
|
||||
({ buildAlibabaVideoGenerationProvider } = await import("./video-generation-provider.js"));
|
||||
});
|
||||
|
||||
installProviderHttpMockCleanup();
|
||||
|
||||
describe("alibaba video generation provider", () => {
|
||||
afterEach(() => {
|
||||
resolveApiKeyForProviderMock.mockClear();
|
||||
postJsonRequestMock.mockReset();
|
||||
fetchWithTimeoutMock.mockReset();
|
||||
assertOkOrThrowHttpErrorMock.mockClear();
|
||||
resolveProviderHttpRequestConfigMock.mockClear();
|
||||
});
|
||||
|
||||
it("submits async Wan generation, polls task status, and downloads the resulting video", async () => {
|
||||
postJsonRequestMock.mockResolvedValue({
|
||||
response: {
|
||||
json: async () => ({
|
||||
request_id: "req-1",
|
||||
output: {
|
||||
task_id: "task-1",
|
||||
},
|
||||
}),
|
||||
},
|
||||
release: vi.fn(async () => {}),
|
||||
});
|
||||
fetchWithTimeoutMock
|
||||
.mockResolvedValueOnce({
|
||||
json: async () => ({
|
||||
output: {
|
||||
task_status: "SUCCEEDED",
|
||||
results: [{ video_url: "https://example.com/out.mp4" }],
|
||||
},
|
||||
}),
|
||||
headers: new Headers(),
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
arrayBuffer: async () => Buffer.from("mp4-bytes"),
|
||||
headers: new Headers({ "content-type": "video/mp4" }),
|
||||
});
|
||||
mockSuccessfulDashscopeVideoTask({ postJsonRequestMock, fetchWithTimeoutMock });
|
||||
|
||||
const provider = buildAlibabaVideoGenerationProvider();
|
||||
const result = await provider.generateVideo({
|
||||
@@ -96,22 +52,8 @@ describe("alibaba video generation provider", () => {
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(fetchWithTimeoutMock).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
"https://dashscope-intl.aliyuncs.com/api/v1/tasks/task-1",
|
||||
expect.objectContaining({ method: "GET" }),
|
||||
120000,
|
||||
fetch,
|
||||
);
|
||||
expect(result.videos).toHaveLength(1);
|
||||
expect(result.videos[0]?.mimeType).toBe("video/mp4");
|
||||
expect(result.metadata).toEqual(
|
||||
expect.objectContaining({
|
||||
requestId: "req-1",
|
||||
taskId: "task-1",
|
||||
taskStatus: "SUCCEEDED",
|
||||
}),
|
||||
);
|
||||
expectDashscopeVideoTaskPoll(fetchWithTimeoutMock);
|
||||
expectSuccessfulDashscopeVideoResult(result);
|
||||
});
|
||||
|
||||
it("fails fast when reference inputs are local buffers instead of remote URLs", async () => {
|
||||
|
||||
@@ -1,29 +1,21 @@
|
||||
import { isProviderApiKeyConfigured } from "openclaw/plugin-sdk/provider-auth";
|
||||
import { resolveApiKeyForProvider } from "openclaw/plugin-sdk/provider-auth-runtime";
|
||||
import { resolveProviderHttpRequestConfig } from "openclaw/plugin-sdk/provider-http";
|
||||
import {
|
||||
assertOkOrThrowHttpError,
|
||||
postJsonRequest,
|
||||
resolveProviderHttpRequestConfig,
|
||||
} from "openclaw/plugin-sdk/provider-http";
|
||||
import {
|
||||
DEFAULT_VIDEO_GENERATION_DURATION_SECONDS,
|
||||
DASHSCOPE_WAN_VIDEO_CAPABILITIES,
|
||||
DASHSCOPE_WAN_VIDEO_MODELS,
|
||||
DEFAULT_DASHSCOPE_WAN_VIDEO_MODEL,
|
||||
DEFAULT_VIDEO_GENERATION_TIMEOUT_MS,
|
||||
DEFAULT_VIDEO_RESOLUTION_TO_SIZE,
|
||||
buildDashscopeVideoGenerationInput,
|
||||
buildDashscopeVideoGenerationParameters,
|
||||
downloadDashscopeGeneratedVideos,
|
||||
extractDashscopeVideoUrls,
|
||||
pollDashscopeVideoTaskUntilComplete,
|
||||
runDashscopeVideoGenerationTask,
|
||||
} from "openclaw/plugin-sdk/video-generation";
|
||||
import type {
|
||||
DashscopeVideoGenerationResponse,
|
||||
VideoGenerationProvider,
|
||||
VideoGenerationRequest,
|
||||
VideoGenerationResult,
|
||||
} from "openclaw/plugin-sdk/video-generation";
|
||||
|
||||
const DEFAULT_ALIBABA_VIDEO_BASE_URL = "https://dashscope-intl.aliyuncs.com";
|
||||
const DEFAULT_ALIBABA_VIDEO_MODEL = "wan2.6-t2v";
|
||||
const DEFAULT_ALIBABA_VIDEO_MODEL = DEFAULT_DASHSCOPE_WAN_VIDEO_MODEL;
|
||||
|
||||
function resolveAlibabaVideoBaseUrl(req: VideoGenerationRequest): string {
|
||||
return req.cfg?.models?.providers?.alibaba?.baseUrl?.trim() || DEFAULT_ALIBABA_VIDEO_BASE_URL;
|
||||
@@ -38,45 +30,13 @@ export function buildAlibabaVideoGenerationProvider(): VideoGenerationProvider {
|
||||
id: "alibaba",
|
||||
label: "Alibaba Model Studio",
|
||||
defaultModel: DEFAULT_ALIBABA_VIDEO_MODEL,
|
||||
models: ["wan2.6-t2v", "wan2.6-i2v", "wan2.6-r2v", "wan2.6-r2v-flash", "wan2.7-r2v"],
|
||||
models: [...DASHSCOPE_WAN_VIDEO_MODELS],
|
||||
isConfigured: ({ agentDir }) =>
|
||||
isProviderApiKeyConfigured({
|
||||
provider: "alibaba",
|
||||
agentDir,
|
||||
}),
|
||||
capabilities: {
|
||||
generate: {
|
||||
maxVideos: 1,
|
||||
maxDurationSeconds: 10,
|
||||
supportsSize: true,
|
||||
supportsAspectRatio: true,
|
||||
supportsResolution: true,
|
||||
supportsAudio: true,
|
||||
supportsWatermark: true,
|
||||
},
|
||||
imageToVideo: {
|
||||
enabled: true,
|
||||
maxVideos: 1,
|
||||
maxInputImages: 1,
|
||||
maxDurationSeconds: 10,
|
||||
supportsSize: true,
|
||||
supportsAspectRatio: true,
|
||||
supportsResolution: true,
|
||||
supportsAudio: true,
|
||||
supportsWatermark: true,
|
||||
},
|
||||
videoToVideo: {
|
||||
enabled: true,
|
||||
maxVideos: 1,
|
||||
maxInputVideos: 4,
|
||||
maxDurationSeconds: 10,
|
||||
supportsSize: true,
|
||||
supportsAspectRatio: true,
|
||||
supportsResolution: true,
|
||||
supportsAudio: true,
|
||||
supportsWatermark: true,
|
||||
},
|
||||
},
|
||||
capabilities: DASHSCOPE_WAN_VIDEO_CAPABILITIES,
|
||||
async generateVideo(req): Promise<VideoGenerationResult> {
|
||||
const fetchFn = fetch;
|
||||
const auth = await resolveApiKeyForProvider({
|
||||
@@ -105,68 +65,19 @@ export function buildAlibabaVideoGenerationProvider(): VideoGenerationProvider {
|
||||
});
|
||||
|
||||
const model = req.model?.trim() || DEFAULT_ALIBABA_VIDEO_MODEL;
|
||||
const { response, release } = await postJsonRequest({
|
||||
return await runDashscopeVideoGenerationTask({
|
||||
providerLabel: "Alibaba Wan",
|
||||
model,
|
||||
req,
|
||||
url: `${resolveDashscopeAigcApiBaseUrl(baseUrl)}/api/v1/services/aigc/video-generation/video-synthesis`,
|
||||
headers,
|
||||
body: {
|
||||
model,
|
||||
input: buildDashscopeVideoGenerationInput({
|
||||
providerLabel: "Alibaba Wan",
|
||||
req,
|
||||
}),
|
||||
parameters: buildDashscopeVideoGenerationParameters(
|
||||
{
|
||||
...req,
|
||||
durationSeconds: req.durationSeconds ?? DEFAULT_VIDEO_GENERATION_DURATION_SECONDS,
|
||||
},
|
||||
DEFAULT_VIDEO_RESOLUTION_TO_SIZE,
|
||||
),
|
||||
},
|
||||
baseUrl: resolveDashscopeAigcApiBaseUrl(baseUrl),
|
||||
timeoutMs: req.timeoutMs,
|
||||
fetchFn,
|
||||
allowPrivateNetwork,
|
||||
dispatcherPolicy,
|
||||
defaultTimeoutMs: DEFAULT_VIDEO_GENERATION_TIMEOUT_MS,
|
||||
});
|
||||
|
||||
try {
|
||||
await assertOkOrThrowHttpError(response, "Alibaba Wan video generation failed");
|
||||
const submitted = (await response.json()) as DashscopeVideoGenerationResponse;
|
||||
const taskId = submitted.output?.task_id?.trim();
|
||||
if (!taskId) {
|
||||
throw new Error("Alibaba Wan video generation response missing task_id");
|
||||
}
|
||||
const completed = await pollDashscopeVideoTaskUntilComplete({
|
||||
providerLabel: "Alibaba Wan",
|
||||
taskId,
|
||||
headers,
|
||||
timeoutMs: req.timeoutMs,
|
||||
fetchFn,
|
||||
baseUrl: resolveDashscopeAigcApiBaseUrl(baseUrl),
|
||||
defaultTimeoutMs: DEFAULT_VIDEO_GENERATION_TIMEOUT_MS,
|
||||
});
|
||||
const urls = extractDashscopeVideoUrls(completed);
|
||||
if (urls.length === 0) {
|
||||
throw new Error("Alibaba Wan video generation completed without output video URLs");
|
||||
}
|
||||
const videos = await downloadDashscopeGeneratedVideos({
|
||||
providerLabel: "Alibaba Wan",
|
||||
urls,
|
||||
timeoutMs: req.timeoutMs,
|
||||
fetchFn,
|
||||
defaultTimeoutMs: DEFAULT_VIDEO_GENERATION_TIMEOUT_MS,
|
||||
});
|
||||
return {
|
||||
videos,
|
||||
model,
|
||||
metadata: {
|
||||
requestId: submitted.request_id,
|
||||
taskId,
|
||||
taskStatus: completed.output?.task_status,
|
||||
},
|
||||
};
|
||||
} finally {
|
||||
await release();
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import type {
|
||||
ModelDefinitionConfig,
|
||||
ModelProviderConfig,
|
||||
} from "openclaw/plugin-sdk/provider-model-shared";
|
||||
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
|
||||
|
||||
const log = createSubsystemLogger("bedrock-mantle-discovery");
|
||||
|
||||
@@ -145,7 +146,7 @@ const REASONING_PATTERNS = [
|
||||
];
|
||||
|
||||
function inferReasoningSupport(modelId: string): boolean {
|
||||
const lower = modelId.toLowerCase();
|
||||
const lower = normalizeLowercaseStringOrEmpty(modelId);
|
||||
return REASONING_PATTERNS.some((p) => lower.includes(p));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/amazon-bedrock-mantle-provider",
|
||||
"version": "2026.4.6",
|
||||
"version": "2026.4.9",
|
||||
"private": true,
|
||||
"description": "OpenClaw Amazon Bedrock Mantle (OpenAI-compatible) provider plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/amazon-bedrock-provider",
|
||||
"version": "2026.4.6",
|
||||
"version": "2026.4.9",
|
||||
"private": true,
|
||||
"description": "OpenClaw Amazon Bedrock provider plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/anthropic-vertex-provider",
|
||||
"version": "2026.4.6",
|
||||
"version": "2026.4.9",
|
||||
"private": true,
|
||||
"description": "OpenClaw Anthropic Vertex provider plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -2,6 +2,7 @@ import type {
|
||||
ModelDefinitionConfig,
|
||||
ModelProviderConfig,
|
||||
} from "openclaw/plugin-sdk/provider-model-shared";
|
||||
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
|
||||
import { resolveAnthropicVertexRegion } from "./region.js";
|
||||
export const ANTHROPIC_VERTEX_DEFAULT_MODEL_ID = "claude-sonnet-4-6";
|
||||
const ANTHROPIC_VERTEX_DEFAULT_CONTEXT_WINDOW = 1_000_000;
|
||||
@@ -52,7 +53,7 @@ export function buildAnthropicVertexProvider(params?: {
|
||||
}): ModelProviderConfig {
|
||||
const region = resolveAnthropicVertexRegion(params?.env);
|
||||
const baseUrl =
|
||||
region.toLowerCase() === "global"
|
||||
normalizeLowercaseStringOrEmpty(region) === "global"
|
||||
? "https://aiplatform.googleapis.com"
|
||||
: `https://${region}-aiplatform.googleapis.com`;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/anthropic-provider",
|
||||
"version": "2026.4.6",
|
||||
"version": "2026.4.9",
|
||||
"private": true,
|
||||
"description": "OpenClaw Anthropic provider plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/arcee-provider",
|
||||
"version": "2026.4.4",
|
||||
"version": "2026.4.9",
|
||||
"private": true,
|
||||
"description": "OpenClaw Arcee provider plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -10,7 +10,7 @@ export default defineBundledChannelEntry({
|
||||
exportName: "bluebubblesPlugin",
|
||||
},
|
||||
secrets: {
|
||||
specifier: "./src/secret-contract.js",
|
||||
specifier: "./secret-contract-api.js",
|
||||
exportName: "channelSecrets",
|
||||
},
|
||||
runtime: {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/bluebubbles",
|
||||
"version": "2026.4.6",
|
||||
"version": "2026.4.9",
|
||||
"description": "OpenClaw BlueBubbles channel plugin",
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
@@ -8,7 +8,7 @@
|
||||
"openclaw": "workspace:*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"openclaw": ">=2026.4.6"
|
||||
"openclaw": ">=2026.4.9"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"openclaw": {
|
||||
@@ -40,13 +40,13 @@
|
||||
"install": {
|
||||
"npmSpec": "@openclaw/bluebubbles",
|
||||
"defaultChoice": "npm",
|
||||
"minHostVersion": ">=2026.4.6"
|
||||
"minHostVersion": ">=2026.4.9"
|
||||
},
|
||||
"compat": {
|
||||
"pluginApi": ">=2026.4.6"
|
||||
"pluginApi": ">=2026.4.9"
|
||||
},
|
||||
"build": {
|
||||
"openclawVersion": "2026.4.6"
|
||||
"openclawVersion": "2026.4.9"
|
||||
},
|
||||
"release": {
|
||||
"publishToClawHub": true,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export {
|
||||
channelSecrets,
|
||||
collectRuntimeConfigAssignments,
|
||||
secretTargetRegistryEntries,
|
||||
} from "./src/secret-contract.js";
|
||||
|
||||
@@ -7,7 +7,7 @@ export default defineBundledChannelSetupEntry({
|
||||
exportName: "bluebubblesSetupPlugin",
|
||||
},
|
||||
secrets: {
|
||||
specifier: "./src/secret-contract.js",
|
||||
specifier: "./secret-contract-api.js",
|
||||
exportName: "channelSecrets",
|
||||
},
|
||||
});
|
||||
|
||||
107
extensions/bluebubbles/src/accounts-normalization.ts
Normal file
107
extensions/bluebubbles/src/accounts-normalization.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import { isBlockedHostnameOrIp } from "openclaw/plugin-sdk/ssrf-runtime";
|
||||
import { normalizeBlueBubblesServerUrl } from "./types.js";
|
||||
|
||||
function asRecord(value: unknown): Record<string, unknown> | null {
|
||||
return value && typeof value === "object" && !Array.isArray(value)
|
||||
? (value as Record<string, unknown>)
|
||||
: null;
|
||||
}
|
||||
|
||||
export function normalizeBlueBubblesPrivateNetworkAliases<T extends object | undefined>(
|
||||
config: T,
|
||||
): T {
|
||||
const record = asRecord(config);
|
||||
if (!record) {
|
||||
return config;
|
||||
}
|
||||
const network = asRecord(record.network);
|
||||
const canonicalValue =
|
||||
typeof network?.dangerouslyAllowPrivateNetwork === "boolean"
|
||||
? network.dangerouslyAllowPrivateNetwork
|
||||
: typeof network?.allowPrivateNetwork === "boolean"
|
||||
? network.allowPrivateNetwork
|
||||
: typeof record.dangerouslyAllowPrivateNetwork === "boolean"
|
||||
? record.dangerouslyAllowPrivateNetwork
|
||||
: typeof record.allowPrivateNetwork === "boolean"
|
||||
? record.allowPrivateNetwork
|
||||
: undefined;
|
||||
|
||||
if (canonicalValue === undefined) {
|
||||
return config;
|
||||
}
|
||||
|
||||
const {
|
||||
allowPrivateNetwork: _legacyFlatAllow,
|
||||
dangerouslyAllowPrivateNetwork: _legacyFlatDanger,
|
||||
...rest
|
||||
} = record;
|
||||
const {
|
||||
allowPrivateNetwork: _legacyNetworkAllow,
|
||||
dangerouslyAllowPrivateNetwork: _legacyNetworkDanger,
|
||||
...restNetwork
|
||||
} = network ?? {};
|
||||
|
||||
return {
|
||||
...rest,
|
||||
network: {
|
||||
...restNetwork,
|
||||
dangerouslyAllowPrivateNetwork: canonicalValue,
|
||||
},
|
||||
} as T;
|
||||
}
|
||||
|
||||
export function normalizeBlueBubblesAccountsMap<T extends object | undefined>(
|
||||
accounts: Record<string, T> | undefined,
|
||||
): Record<string, T> | undefined {
|
||||
if (!accounts) {
|
||||
return undefined;
|
||||
}
|
||||
return Object.fromEntries(
|
||||
Object.entries(accounts).map(([accountKey, accountConfig]) => [
|
||||
accountKey,
|
||||
normalizeBlueBubblesPrivateNetworkAliases(accountConfig),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveBlueBubblesPrivateNetworkConfigValue(
|
||||
config: object | null | undefined,
|
||||
): boolean | undefined {
|
||||
const record = asRecord(config);
|
||||
if (!record) {
|
||||
return undefined;
|
||||
}
|
||||
const network = asRecord(record.network);
|
||||
if (typeof network?.dangerouslyAllowPrivateNetwork === "boolean") {
|
||||
return network.dangerouslyAllowPrivateNetwork;
|
||||
}
|
||||
if (typeof network?.allowPrivateNetwork === "boolean") {
|
||||
return network.allowPrivateNetwork;
|
||||
}
|
||||
if (typeof record.dangerouslyAllowPrivateNetwork === "boolean") {
|
||||
return record.dangerouslyAllowPrivateNetwork;
|
||||
}
|
||||
if (typeof record.allowPrivateNetwork === "boolean") {
|
||||
return record.allowPrivateNetwork;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function resolveBlueBubblesEffectiveAllowPrivateNetworkFromConfig(params: {
|
||||
baseUrl?: string;
|
||||
config?: object | null;
|
||||
}): boolean {
|
||||
const configuredValue = resolveBlueBubblesPrivateNetworkConfigValue(params.config);
|
||||
if (configuredValue !== undefined) {
|
||||
return configuredValue;
|
||||
}
|
||||
if (!params.baseUrl) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
const hostname = new URL(normalizeBlueBubblesServerUrl(params.baseUrl)).hostname.trim();
|
||||
return Boolean(hostname) && isBlockedHostnameOrIp(hostname);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -4,9 +4,14 @@ import {
|
||||
resolveMergedAccountConfig,
|
||||
} from "openclaw/plugin-sdk/account-resolution";
|
||||
import { resolveChannelStreamingChunkMode } from "openclaw/plugin-sdk/channel-streaming";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
|
||||
import { isBlockedHostnameOrIp } from "openclaw/plugin-sdk/ssrf-runtime";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
|
||||
import {
|
||||
normalizeBlueBubblesAccountsMap,
|
||||
normalizeBlueBubblesPrivateNetworkAliases,
|
||||
resolveBlueBubblesEffectiveAllowPrivateNetworkFromConfig,
|
||||
resolveBlueBubblesPrivateNetworkConfigValue as resolveBlueBubblesPrivateNetworkConfigValueFromRecord,
|
||||
} from "./accounts-normalization.js";
|
||||
import { hasConfiguredSecretInput, normalizeSecretInputString } from "./secret-input.js";
|
||||
import { normalizeBlueBubblesServerUrl, type BlueBubblesAccountConfig } from "./types.js";
|
||||
|
||||
@@ -25,76 +30,13 @@ const {
|
||||
} = createAccountListHelpers("bluebubbles");
|
||||
export { listBlueBubblesAccountIds, resolveDefaultBlueBubblesAccountId };
|
||||
|
||||
function asRecord(value: unknown): Record<string, unknown> | null {
|
||||
return value && typeof value === "object" && !Array.isArray(value)
|
||||
? (value as Record<string, unknown>)
|
||||
: null;
|
||||
}
|
||||
|
||||
function normalizeBlueBubblesPrivateNetworkAliases(
|
||||
config: Record<string, unknown> | undefined,
|
||||
): Record<string, unknown> | undefined {
|
||||
const record = asRecord(config);
|
||||
if (!record) {
|
||||
return config;
|
||||
}
|
||||
const network = asRecord(record.network);
|
||||
const canonicalValue =
|
||||
typeof network?.dangerouslyAllowPrivateNetwork === "boolean"
|
||||
? network.dangerouslyAllowPrivateNetwork
|
||||
: typeof network?.allowPrivateNetwork === "boolean"
|
||||
? network.allowPrivateNetwork
|
||||
: typeof record.dangerouslyAllowPrivateNetwork === "boolean"
|
||||
? record.dangerouslyAllowPrivateNetwork
|
||||
: typeof record.allowPrivateNetwork === "boolean"
|
||||
? record.allowPrivateNetwork
|
||||
: undefined;
|
||||
|
||||
if (canonicalValue === undefined) {
|
||||
return config;
|
||||
}
|
||||
|
||||
const {
|
||||
allowPrivateNetwork: _legacyFlatAllow,
|
||||
dangerouslyAllowPrivateNetwork: _legacyFlatDanger,
|
||||
...rest
|
||||
} = record;
|
||||
const {
|
||||
allowPrivateNetwork: _legacyNetworkAllow,
|
||||
dangerouslyAllowPrivateNetwork: _legacyNetworkDanger,
|
||||
...restNetwork
|
||||
} = network ?? {};
|
||||
|
||||
return {
|
||||
...rest,
|
||||
network: {
|
||||
...restNetwork,
|
||||
dangerouslyAllowPrivateNetwork: canonicalValue,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeBlueBubblesAccountsMap(
|
||||
accounts: Record<string, Partial<BlueBubblesAccountConfig>> | undefined,
|
||||
): Record<string, Partial<BlueBubblesAccountConfig>> | undefined {
|
||||
if (!accounts) {
|
||||
return undefined;
|
||||
}
|
||||
return Object.fromEntries(
|
||||
Object.entries(accounts).map(([accountKey, accountConfig]) => [
|
||||
accountKey,
|
||||
normalizeBlueBubblesPrivateNetworkAliases(accountConfig) as Partial<BlueBubblesAccountConfig>,
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
function mergeBlueBubblesAccountConfig(
|
||||
cfg: OpenClawConfig,
|
||||
accountId: string,
|
||||
): BlueBubblesAccountConfig {
|
||||
const channelConfig = normalizeBlueBubblesPrivateNetworkAliases(
|
||||
cfg.channels?.bluebubbles as BlueBubblesAccountConfig | undefined,
|
||||
) as BlueBubblesAccountConfig | undefined;
|
||||
);
|
||||
const accounts = normalizeBlueBubblesAccountsMap(
|
||||
cfg.channels?.bluebubbles?.accounts as
|
||||
| Record<string, Partial<BlueBubblesAccountConfig>>
|
||||
@@ -141,43 +83,14 @@ export function resolveBlueBubblesAccount(params: {
|
||||
export function resolveBlueBubblesPrivateNetworkConfigValue(
|
||||
config: BlueBubblesAccountConfig | null | undefined,
|
||||
): boolean | undefined {
|
||||
const record = asRecord(config);
|
||||
if (!record) {
|
||||
return undefined;
|
||||
}
|
||||
const network = asRecord(record.network);
|
||||
if (typeof network?.dangerouslyAllowPrivateNetwork === "boolean") {
|
||||
return network.dangerouslyAllowPrivateNetwork;
|
||||
}
|
||||
if (typeof network?.allowPrivateNetwork === "boolean") {
|
||||
return network.allowPrivateNetwork;
|
||||
}
|
||||
if (typeof record.dangerouslyAllowPrivateNetwork === "boolean") {
|
||||
return record.dangerouslyAllowPrivateNetwork;
|
||||
}
|
||||
if (typeof record.allowPrivateNetwork === "boolean") {
|
||||
return record.allowPrivateNetwork;
|
||||
}
|
||||
return undefined;
|
||||
return resolveBlueBubblesPrivateNetworkConfigValueFromRecord(config);
|
||||
}
|
||||
|
||||
export function resolveBlueBubblesEffectiveAllowPrivateNetwork(params: {
|
||||
baseUrl?: string;
|
||||
config?: BlueBubblesAccountConfig | null;
|
||||
}): boolean {
|
||||
const configuredValue = resolveBlueBubblesPrivateNetworkConfigValue(params.config);
|
||||
if (configuredValue !== undefined) {
|
||||
return configuredValue;
|
||||
}
|
||||
if (!params.baseUrl) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
const hostname = new URL(normalizeBlueBubblesServerUrl(params.baseUrl)).hostname.trim();
|
||||
return Boolean(hostname) && isBlockedHostnameOrIp(hostname);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
return resolveBlueBubblesEffectiveAllowPrivateNetworkFromConfig(params);
|
||||
}
|
||||
|
||||
export function listEnabledBlueBubblesAccounts(cfg: OpenClawConfig): ResolvedBlueBubblesAccount[] {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ChannelPlugin } from "openclaw/plugin-sdk/core";
|
||||
import type { ChannelPlugin } from "openclaw/plugin-sdk/channel-core";
|
||||
import { type ResolvedBlueBubblesAccount } from "./accounts.js";
|
||||
import {
|
||||
bluebubblesCapabilities,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/account-id";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
|
||||
type BlueBubblesConfigPatch = {
|
||||
serverUrl?: string;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ChannelConfigUiHint } from "openclaw/plugin-sdk/core";
|
||||
import type { ChannelConfigUiHint } from "openclaw/plugin-sdk/channel-core";
|
||||
|
||||
export const bluebubblesChannelConfigUiHints = {
|
||||
"": {
|
||||
|
||||
@@ -3,6 +3,7 @@ import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { resolveChannelMediaMaxBytes } from "openclaw/plugin-sdk/media-runtime";
|
||||
import { lowercasePreservingWhitespace } from "openclaw/plugin-sdk/text-runtime";
|
||||
import { resolveBlueBubblesAccount } from "./accounts.js";
|
||||
import { sendBlueBubblesAttachment } from "./attachments.js";
|
||||
import { basenameFromMediaSource, safeFileURLToPath } from "./local-file-access.js";
|
||||
@@ -73,9 +74,9 @@ function isPathInsideRoot(candidate: string, root: string): boolean {
|
||||
? normalizedRoot
|
||||
: normalizedRoot + path.sep;
|
||||
if (process.platform === "win32") {
|
||||
const candidateLower = normalizedCandidate.toLowerCase();
|
||||
const rootLower = normalizedRoot.toLowerCase();
|
||||
const rootWithSepLower = rootWithSep.toLowerCase();
|
||||
const candidateLower = lowercasePreservingWhitespace(normalizedCandidate);
|
||||
const rootLower = lowercasePreservingWhitespace(normalizedRoot);
|
||||
const rootWithSepLower = lowercasePreservingWhitespace(rootWithSep);
|
||||
return candidateLower === rootLower || candidateLower.startsWith(rootWithSepLower);
|
||||
}
|
||||
return normalizedCandidate === normalizedRoot || normalizedCandidate.startsWith(rootWithSep);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
import type { ResolvedBlueBubblesAccount } from "./accounts.js";
|
||||
import { getBlueBubblesRuntime } from "./runtime.js";
|
||||
export {
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { isBlockedHostnameOrIp } from "openclaw/plugin-sdk/ssrf-runtime";
|
||||
import type { Mock } from "vitest";
|
||||
import { afterEach, beforeEach, vi } from "vitest";
|
||||
import { _setFetchGuardForTesting, normalizeBlueBubblesServerUrl } from "./types.js";
|
||||
import {
|
||||
normalizeBlueBubblesAccountsMap,
|
||||
normalizeBlueBubblesPrivateNetworkAliases,
|
||||
resolveBlueBubblesEffectiveAllowPrivateNetworkFromConfig,
|
||||
resolveBlueBubblesPrivateNetworkConfigValue as resolveBlueBubblesPrivateNetworkConfigValueFromConfig,
|
||||
} from "./accounts-normalization.js";
|
||||
import { _setFetchGuardForTesting } from "./types.js";
|
||||
|
||||
export const BLUE_BUBBLES_PRIVATE_API_STATUS = {
|
||||
enabled: true,
|
||||
@@ -28,69 +33,6 @@ export function mockBlueBubblesPrivateApiStatusOnce(
|
||||
mock.mockReturnValueOnce(value);
|
||||
}
|
||||
|
||||
function asRecord(value: unknown): Record<string, unknown> | null {
|
||||
return value && typeof value === "object" && !Array.isArray(value)
|
||||
? (value as Record<string, unknown>)
|
||||
: null;
|
||||
}
|
||||
|
||||
function normalizeBlueBubblesPrivateNetworkAliases(
|
||||
config: Record<string, unknown> | undefined,
|
||||
): Record<string, unknown> | undefined {
|
||||
const record = asRecord(config);
|
||||
if (!record) {
|
||||
return config;
|
||||
}
|
||||
const network = asRecord(record.network);
|
||||
const canonicalValue =
|
||||
typeof network?.dangerouslyAllowPrivateNetwork === "boolean"
|
||||
? network.dangerouslyAllowPrivateNetwork
|
||||
: typeof network?.allowPrivateNetwork === "boolean"
|
||||
? network.allowPrivateNetwork
|
||||
: typeof record.dangerouslyAllowPrivateNetwork === "boolean"
|
||||
? record.dangerouslyAllowPrivateNetwork
|
||||
: typeof record.allowPrivateNetwork === "boolean"
|
||||
? record.allowPrivateNetwork
|
||||
: undefined;
|
||||
|
||||
if (canonicalValue === undefined) {
|
||||
return config;
|
||||
}
|
||||
|
||||
const {
|
||||
allowPrivateNetwork: _legacyFlatAllow,
|
||||
dangerouslyAllowPrivateNetwork: _legacyFlatDanger,
|
||||
...rest
|
||||
} = record;
|
||||
const {
|
||||
allowPrivateNetwork: _legacyNetworkAllow,
|
||||
dangerouslyAllowPrivateNetwork: _legacyNetworkDanger,
|
||||
...restNetwork
|
||||
} = network ?? {};
|
||||
|
||||
return {
|
||||
...rest,
|
||||
network: {
|
||||
...restNetwork,
|
||||
dangerouslyAllowPrivateNetwork: canonicalValue,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeBlueBubblesAccountsMap(
|
||||
accounts: Record<string, Record<string, unknown> | undefined> | undefined,
|
||||
): Record<string, Record<string, unknown> | undefined> | undefined {
|
||||
if (!accounts) {
|
||||
return undefined;
|
||||
}
|
||||
return Object.fromEntries(
|
||||
Object.entries(accounts).map(([accountKey, accountConfig]) => [
|
||||
accountKey,
|
||||
normalizeBlueBubblesPrivateNetworkAliases(accountConfig),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveBlueBubblesAccountFromConfig(params: {
|
||||
cfg?: { channels?: { bluebubbles?: Record<string, unknown> } };
|
||||
accountId?: string;
|
||||
@@ -127,48 +69,6 @@ export function resolveBlueBubblesAccountFromConfig(params: {
|
||||
};
|
||||
}
|
||||
|
||||
function resolveBlueBubblesPrivateNetworkConfigValueFromConfig(
|
||||
config: Record<string, unknown> | undefined,
|
||||
): boolean | undefined {
|
||||
const record = asRecord(config);
|
||||
if (!record) {
|
||||
return undefined;
|
||||
}
|
||||
const network = asRecord(record.network);
|
||||
if (typeof network?.dangerouslyAllowPrivateNetwork === "boolean") {
|
||||
return network.dangerouslyAllowPrivateNetwork;
|
||||
}
|
||||
if (typeof network?.allowPrivateNetwork === "boolean") {
|
||||
return network.allowPrivateNetwork;
|
||||
}
|
||||
if (typeof record.dangerouslyAllowPrivateNetwork === "boolean") {
|
||||
return record.dangerouslyAllowPrivateNetwork;
|
||||
}
|
||||
if (typeof record.allowPrivateNetwork === "boolean") {
|
||||
return record.allowPrivateNetwork;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function resolveBlueBubblesEffectiveAllowPrivateNetworkFromConfig(params: {
|
||||
baseUrl?: string;
|
||||
config?: Record<string, unknown>;
|
||||
}) {
|
||||
const configuredValue = resolveBlueBubblesPrivateNetworkConfigValueFromConfig(params.config);
|
||||
if (configuredValue !== undefined) {
|
||||
return configuredValue;
|
||||
}
|
||||
if (!params.baseUrl) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
const hostname = new URL(normalizeBlueBubblesServerUrl(params.baseUrl)).hostname.trim();
|
||||
return Boolean(hostname) && isBlockedHostnameOrIp(hostname);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function createBlueBubblesAccountsMockModule() {
|
||||
return {
|
||||
resolveBlueBubblesAccount: vi.fn(resolveBlueBubblesAccountFromConfig),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/brave-plugin",
|
||||
"version": "2026.4.6",
|
||||
"version": "2026.4.9",
|
||||
"private": true,
|
||||
"description": "OpenClaw Brave plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
|
||||
import {
|
||||
normalizeLowercaseStringOrEmpty,
|
||||
normalizeOptionalString,
|
||||
} from "openclaw/plugin-sdk/text-runtime";
|
||||
|
||||
export type BraveConfig = {
|
||||
mode?: string;
|
||||
@@ -126,7 +129,8 @@ function normalizeBraveSearchLang(value: string | undefined): string | undefined
|
||||
if (!trimmed) {
|
||||
return undefined;
|
||||
}
|
||||
const canonical = BRAVE_SEARCH_LANG_ALIASES[trimmed.toLowerCase()] ?? trimmed.toLowerCase();
|
||||
const lower = normalizeLowercaseStringOrEmpty(trimmed);
|
||||
const canonical = BRAVE_SEARCH_LANG_ALIASES[lower] ?? lower;
|
||||
if (!BRAVE_SEARCH_LANG_CODES.has(canonical)) {
|
||||
return undefined;
|
||||
}
|
||||
@@ -158,7 +162,7 @@ function normalizeBraveUiLang(value: string | undefined): string | undefined {
|
||||
return undefined;
|
||||
}
|
||||
const [, language, region] = match;
|
||||
return `${language.toLowerCase()}-${region.toUpperCase()}`;
|
||||
return `${normalizeLowercaseStringOrEmpty(language)}-${region.toUpperCase()}`;
|
||||
}
|
||||
|
||||
export function resolveBraveConfig(searchConfig?: Record<string, unknown>): BraveConfig {
|
||||
|
||||
@@ -1,21 +1,11 @@
|
||||
import {
|
||||
resolveProviderWebSearchPluginConfig,
|
||||
setProviderWebSearchPluginConfigValue,
|
||||
createWebSearchProviderContractFields,
|
||||
type WebSearchProviderPlugin,
|
||||
} from "openclaw/plugin-sdk/provider-web-search-contract";
|
||||
|
||||
function getTopLevelCredentialValue(searchConfig?: Record<string, unknown>): unknown {
|
||||
return searchConfig?.apiKey;
|
||||
}
|
||||
|
||||
function setTopLevelCredentialValue(
|
||||
searchConfigTarget: Record<string, unknown>,
|
||||
value: unknown,
|
||||
): void {
|
||||
searchConfigTarget.apiKey = value;
|
||||
}
|
||||
} from "openclaw/plugin-sdk/provider-web-search-config-contract";
|
||||
|
||||
export function createBraveWebSearchProvider(): WebSearchProviderPlugin {
|
||||
const credentialPath = "plugins.entries.brave.config.webSearch.apiKey";
|
||||
|
||||
return {
|
||||
id: "brave",
|
||||
label: "Brave Search",
|
||||
@@ -27,15 +17,12 @@ export function createBraveWebSearchProvider(): WebSearchProviderPlugin {
|
||||
signupUrl: "https://brave.com/search/api/",
|
||||
docsUrl: "https://docs.openclaw.ai/brave-search",
|
||||
autoDetectOrder: 10,
|
||||
credentialPath: "plugins.entries.brave.config.webSearch.apiKey",
|
||||
inactiveSecretPaths: ["plugins.entries.brave.config.webSearch.apiKey"],
|
||||
getCredentialValue: getTopLevelCredentialValue,
|
||||
setCredentialValue: setTopLevelCredentialValue,
|
||||
getConfiguredCredentialValue: (config) =>
|
||||
resolveProviderWebSearchPluginConfig(config, "brave")?.apiKey,
|
||||
setConfiguredCredentialValue: (configTarget, value) => {
|
||||
setProviderWebSearchPluginConfigValue(configTarget, "brave", "apiKey", value);
|
||||
},
|
||||
credentialPath,
|
||||
...createWebSearchProviderContractFields({
|
||||
credentialPath,
|
||||
searchCredential: { type: "top-level" },
|
||||
configuredCredential: { pluginId: "brave" },
|
||||
}),
|
||||
createTool: () => null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { definePluginEntry } from "openclaw/plugin-sdk/core";
|
||||
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
|
||||
|
||||
export default definePluginEntry({
|
||||
id: "browser",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/browser-plugin",
|
||||
"version": "2026.4.6",
|
||||
"version": "2026.4.9",
|
||||
"private": true,
|
||||
"description": "OpenClaw browser tool plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type {
|
||||
OpenClawPluginApi,
|
||||
OpenClawPluginNodeHostCommand,
|
||||
OpenClawPluginToolContext,
|
||||
OpenClawPluginToolFactory,
|
||||
} from "openclaw/plugin-sdk/plugin-entry";
|
||||
@@ -14,7 +15,7 @@ import {
|
||||
|
||||
export const browserPluginReload = { restartPrefixes: ["browser"] };
|
||||
|
||||
export const browserPluginNodeHostCommands = [
|
||||
export const browserPluginNodeHostCommands: OpenClawPluginNodeHostCommand[] = [
|
||||
{
|
||||
command: "browser.proxy",
|
||||
cap: "browser",
|
||||
@@ -22,7 +23,7 @@ export const browserPluginNodeHostCommands = [
|
||||
},
|
||||
];
|
||||
|
||||
export const browserSecurityAuditCollectors = [collectBrowserSecurityAuditFindings] as const;
|
||||
export const browserSecurityAuditCollectors = [collectBrowserSecurityAuditFindings];
|
||||
|
||||
export function registerBrowserPlugin(api: OpenClawPluginApi) {
|
||||
api.registerTool(((ctx: OpenClawPluginToolContext) =>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
|
||||
|
||||
type BridgeAuth = {
|
||||
token?: string;
|
||||
password?: string;
|
||||
@@ -11,8 +13,8 @@ export function setBridgeAuthForPort(port: number, auth: BridgeAuth): void {
|
||||
if (!Number.isFinite(port) || port <= 0) {
|
||||
return;
|
||||
}
|
||||
const token = typeof auth.token === "string" ? auth.token.trim() : "";
|
||||
const password = typeof auth.password === "string" ? auth.password.trim() : "";
|
||||
const token = normalizeOptionalString(auth.token) ?? "";
|
||||
const password = normalizeOptionalString(auth.password) ?? "";
|
||||
authByPort.set(port, {
|
||||
token: token || undefined,
|
||||
password: password || undefined,
|
||||
|
||||
@@ -380,6 +380,7 @@ export async function snapshotDom(opts: {
|
||||
const expression = `(() => {
|
||||
const maxNodes = ${JSON.stringify(limit)};
|
||||
const maxText = ${JSON.stringify(maxTextChars)};
|
||||
const lower = (value) => String(value || "").toLocaleLowerCase();
|
||||
const nodes = [];
|
||||
const root = document.documentElement;
|
||||
if (!root) return { nodes };
|
||||
@@ -389,7 +390,7 @@ export async function snapshotDom(opts: {
|
||||
const el = cur.el;
|
||||
if (!el || el.nodeType !== 1) continue;
|
||||
const ref = "n" + String(nodes.length + 1);
|
||||
const tag = (el.tagName || "").toLowerCase();
|
||||
const tag = lower(el.tagName);
|
||||
const id = el.id ? String(el.id) : undefined;
|
||||
const className = el.className ? String(el.className).slice(0, 300) : undefined;
|
||||
const role = el.getAttribute && el.getAttribute("role") ? String(el.getAttribute("role")) : undefined;
|
||||
@@ -510,9 +511,10 @@ export async function querySelector(opts: {
|
||||
const lim = ${JSON.stringify(limit)};
|
||||
const maxText = ${JSON.stringify(maxText)};
|
||||
const maxHtml = ${JSON.stringify(maxHtml)};
|
||||
const lower = (value) => String(value || "").toLocaleLowerCase();
|
||||
const els = Array.from(document.querySelectorAll(sel)).slice(0, lim);
|
||||
return els.map((el, i) => {
|
||||
const tag = (el.tagName || "").toLowerCase();
|
||||
const tag = lower(el.tagName);
|
||||
const id = el.id ? String(el.id) : undefined;
|
||||
const className = el.className ? String(el.className).slice(0, 300) : undefined;
|
||||
let text = "";
|
||||
|
||||
@@ -2,7 +2,10 @@ import { execFileSync } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
|
||||
import {
|
||||
normalizeLowercaseStringOrEmpty,
|
||||
normalizeOptionalString,
|
||||
} from "openclaw/plugin-sdk/text-runtime";
|
||||
import type { ResolvedBrowserConfig } from "./config.js";
|
||||
|
||||
export type BrowserExecutable = {
|
||||
@@ -115,7 +118,7 @@ function execText(
|
||||
encoding: "utf8",
|
||||
maxBuffer,
|
||||
});
|
||||
return String(output ?? "").trim() || null;
|
||||
return normalizeOptionalString(output) ?? null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
@@ -192,7 +195,7 @@ function detectDefaultChromiumExecutableMac(): BrowserExecutable | null {
|
||||
if (!appPathRaw) {
|
||||
return null;
|
||||
}
|
||||
const appPath = appPathRaw.trim().replace(/\/$/, "");
|
||||
const appPath = appPathRaw.replace(/\/$/, "");
|
||||
const exeName = execText("/usr/bin/defaults", [
|
||||
"read",
|
||||
path.join(appPath, "Contents", "Info"),
|
||||
@@ -201,7 +204,7 @@ function detectDefaultChromiumExecutableMac(): BrowserExecutable | null {
|
||||
if (!exeName) {
|
||||
return null;
|
||||
}
|
||||
const exePath = path.join(appPath, "Contents", "MacOS", exeName.trim());
|
||||
const exePath = path.join(appPath, "Contents", "MacOS", exeName);
|
||||
if (!exists(exePath)) {
|
||||
return null;
|
||||
}
|
||||
@@ -430,12 +433,12 @@ function readWindowsCommandForProgId(progId: string): string | null {
|
||||
return null;
|
||||
}
|
||||
const match = output.match(/REG_\w+\s+(.+)$/im);
|
||||
return match?.[1]?.trim() || null;
|
||||
return normalizeOptionalString(match?.[1]) ?? null;
|
||||
}
|
||||
|
||||
function expandWindowsEnvVars(value: string): string {
|
||||
return value.replace(/%([^%]+)%/g, (_match, name) => {
|
||||
const key = String(name ?? "").trim();
|
||||
const key = normalizeOptionalString(name) ?? "";
|
||||
return key ? (process.env[key] ?? `%${key}%`) : _match;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { type ChildProcess, type ChildProcessWithoutNullStreams, spawn } from "n
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
|
||||
import type { SsrFPolicy } from "../infra/net/ssrf.js";
|
||||
import { ensurePortAvailable } from "../infra/ports.js";
|
||||
import { rawDataToString } from "../infra/ws.js";
|
||||
@@ -196,7 +197,7 @@ export async function getChromeWebSocketUrl(
|
||||
return cdpUrl;
|
||||
}
|
||||
const version = await fetchChromeVersion(cdpUrl, timeoutMs, ssrfPolicy);
|
||||
const wsUrl = String(version?.webSocketDebuggerUrl ?? "").trim();
|
||||
const wsUrl = normalizeOptionalString(version?.webSocketDebuggerUrl) ?? "";
|
||||
if (!wsUrl) {
|
||||
return null;
|
||||
}
|
||||
@@ -409,7 +410,8 @@ export async function launchOpenClawChrome(
|
||||
}
|
||||
|
||||
if (!(await isChromeReachable(profile.cdpUrl))) {
|
||||
const stderrOutput = Buffer.concat(stderrChunks).toString("utf8").trim();
|
||||
const stderrOutput =
|
||||
normalizeOptionalString(Buffer.concat(stderrChunks).toString("utf8")) ?? "";
|
||||
const stderrHint = stderrOutput
|
||||
? `\nChrome stderr:\n${stderrOutput.slice(0, CHROME_STDERR_HINT_MAX_CHARS)}`
|
||||
: "";
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
|
||||
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
|
||||
import { formatCliCommand } from "../cli/command-format.js";
|
||||
import { loadConfig } from "../config/config.js";
|
||||
@@ -141,8 +142,9 @@ function resolveBrowserFetchOperatorHint(url: string): string {
|
||||
}
|
||||
|
||||
function normalizeErrorMessage(err: unknown): string {
|
||||
if (err instanceof Error && err.message.trim().length > 0) {
|
||||
return err.message.trim();
|
||||
const message = err instanceof Error ? normalizeOptionalString(err.message) : undefined;
|
||||
if (message) {
|
||||
return message;
|
||||
}
|
||||
return String(err);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
|
||||
import {
|
||||
normalizeLowercaseStringOrEmpty,
|
||||
normalizeOptionalString,
|
||||
} from "openclaw/plugin-sdk/text-runtime";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { loadConfig } from "../config/config.js";
|
||||
import { resolveGatewayAuth } from "../gateway/auth.js";
|
||||
@@ -18,8 +21,8 @@ export function resolveBrowserControlAuth(
|
||||
env,
|
||||
tailscaleMode: cfg?.gateway?.tailscale?.mode,
|
||||
});
|
||||
const token = typeof auth.token === "string" ? auth.token.trim() : "";
|
||||
const password = typeof auth.password === "string" ? auth.password.trim() : "";
|
||||
const token = normalizeOptionalString(auth.token) ?? "";
|
||||
const password = normalizeOptionalString(auth.password) ?? "";
|
||||
return {
|
||||
token: token || undefined,
|
||||
password: password || undefined,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
|
||||
import type { BrowserFormField } from "./client-actions-core.js";
|
||||
|
||||
export const DEFAULT_FILL_FIELD_TYPE = "text";
|
||||
@@ -5,11 +6,11 @@ export const DEFAULT_FILL_FIELD_TYPE = "text";
|
||||
type BrowserFormFieldValue = NonNullable<BrowserFormField["value"]>;
|
||||
|
||||
export function normalizeBrowserFormFieldRef(value: unknown): string {
|
||||
return typeof value === "string" ? value.trim() : "";
|
||||
return normalizeOptionalString(value) ?? "";
|
||||
}
|
||||
|
||||
export function normalizeBrowserFormFieldType(value: unknown): string {
|
||||
const type = typeof value === "string" ? value.trim() : "";
|
||||
const type = normalizeOptionalString(value) ?? "";
|
||||
return type || DEFAULT_FILL_FIELD_TYPE;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
|
||||
import { hasProxyEnvConfigured } from "../infra/net/proxy-env.js";
|
||||
import {
|
||||
isPrivateNetworkAllowedByPolicy,
|
||||
@@ -46,7 +47,7 @@ export async function assertBrowserNavigationAllowed(
|
||||
lookupFn?: LookupFn;
|
||||
} & BrowserNavigationPolicyOptions,
|
||||
): Promise<void> {
|
||||
const rawUrl = String(opts.url ?? "").trim();
|
||||
const rawUrl = normalizeOptionalString(opts.url) ?? "";
|
||||
if (!rawUrl) {
|
||||
throw new InvalidBrowserNavigationUrlError("url is required");
|
||||
}
|
||||
@@ -94,7 +95,7 @@ export async function assertBrowserNavigationResultAllowed(
|
||||
lookupFn?: LookupFn;
|
||||
} & BrowserNavigationPolicyOptions,
|
||||
): Promise<void> {
|
||||
const rawUrl = String(opts.url ?? "").trim();
|
||||
const rawUrl = normalizeOptionalString(opts.url) ?? "";
|
||||
if (!rawUrl) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
|
||||
import type {
|
||||
Browser,
|
||||
BrowserContext,
|
||||
@@ -146,7 +147,7 @@ function roleRefsKey(cdpUrl: string, targetId: string) {
|
||||
}
|
||||
|
||||
function isBlockedTarget(cdpUrl: string, targetId?: string): boolean {
|
||||
const normalizedTargetId = targetId?.trim() || "";
|
||||
const normalizedTargetId = normalizeOptionalString(targetId) ?? "";
|
||||
if (!normalizedTargetId) {
|
||||
return false;
|
||||
}
|
||||
@@ -154,7 +155,7 @@ function isBlockedTarget(cdpUrl: string, targetId?: string): boolean {
|
||||
}
|
||||
|
||||
function markTargetBlocked(cdpUrl: string, targetId?: string): void {
|
||||
const normalizedTargetId = targetId?.trim() || "";
|
||||
const normalizedTargetId = normalizeOptionalString(targetId) ?? "";
|
||||
if (!normalizedTargetId) {
|
||||
return;
|
||||
}
|
||||
@@ -162,7 +163,7 @@ function markTargetBlocked(cdpUrl: string, targetId?: string): void {
|
||||
}
|
||||
|
||||
function clearBlockedTarget(cdpUrl: string, targetId?: string): void {
|
||||
const normalizedTargetId = targetId?.trim() || "";
|
||||
const normalizedTargetId = normalizeOptionalString(targetId) ?? "";
|
||||
if (!normalizedTargetId) {
|
||||
return;
|
||||
}
|
||||
@@ -237,7 +238,7 @@ export function rememberRoleRefsForTarget(opts: {
|
||||
frameSelector?: string;
|
||||
mode?: NonNullable<PageState["roleRefsMode"]>;
|
||||
}): void {
|
||||
const targetId = opts.targetId.trim();
|
||||
const targetId = normalizeOptionalString(opts.targetId) ?? "";
|
||||
if (!targetId) {
|
||||
return;
|
||||
}
|
||||
@@ -267,12 +268,13 @@ export function storeRoleRefsForTarget(opts: {
|
||||
state.roleRefs = opts.refs;
|
||||
state.roleRefsFrameSelector = opts.frameSelector;
|
||||
state.roleRefsMode = opts.mode;
|
||||
if (!opts.targetId?.trim()) {
|
||||
const targetId = normalizeOptionalString(opts.targetId);
|
||||
if (!targetId) {
|
||||
return;
|
||||
}
|
||||
rememberRoleRefsForTarget({
|
||||
cdpUrl: opts.cdpUrl,
|
||||
targetId: opts.targetId,
|
||||
targetId,
|
||||
refs: opts.refs,
|
||||
frameSelector: opts.frameSelector,
|
||||
mode: opts.mode,
|
||||
@@ -284,7 +286,7 @@ export function restoreRoleRefsForTarget(opts: {
|
||||
targetId?: string;
|
||||
page: Page;
|
||||
}): void {
|
||||
const targetId = opts.targetId?.trim() || "";
|
||||
const targetId = normalizeOptionalString(opts.targetId) ?? "";
|
||||
if (!targetId) {
|
||||
return;
|
||||
}
|
||||
@@ -523,7 +525,7 @@ async function pageTargetId(page: Page): Promise<string | null> {
|
||||
const session = await page.context().newCDPSession(page);
|
||||
try {
|
||||
const info = (await session.send("Target.getTargetInfo")) as TargetInfoResponse;
|
||||
const targetId = String(info?.targetInfo?.targetId ?? "").trim();
|
||||
const targetId = normalizeOptionalString(info?.targetInfo?.targetId) ?? "";
|
||||
return targetId || null;
|
||||
} finally {
|
||||
await session.detach().catch(() => {});
|
||||
@@ -703,7 +705,7 @@ async function closeBlockedNavigationTarget(opts: {
|
||||
// Quarantine the concrete page first; then persist by target id when available.
|
||||
markPageRefBlocked(opts.cdpUrl, opts.page);
|
||||
const resolvedTargetId = await pageTargetId(opts.page).catch(() => null);
|
||||
const fallbackTargetId = opts.targetId?.trim() || "";
|
||||
const fallbackTargetId = normalizeOptionalString(opts.targetId) ?? "";
|
||||
const targetIdToBlock = resolvedTargetId || fallbackTargetId;
|
||||
if (targetIdToBlock) {
|
||||
markTargetBlocked(opts.cdpUrl, targetIdToBlock);
|
||||
@@ -899,8 +901,9 @@ async function tryTerminateExecutionViaCdp(opts: {
|
||||
return;
|
||||
}
|
||||
|
||||
const target = pages.find((p) => String(p.id ?? "").trim() === opts.targetId);
|
||||
const wsUrlRaw = String(target?.webSocketDebuggerUrl ?? "").trim();
|
||||
const targetId = normalizeOptionalString(opts.targetId) ?? "";
|
||||
const target = pages.find((p) => normalizeOptionalString(p.id) === targetId);
|
||||
const wsUrlRaw = normalizeOptionalString(target?.webSocketDebuggerUrl) ?? "";
|
||||
if (!wsUrlRaw) {
|
||||
return;
|
||||
}
|
||||
@@ -931,8 +934,9 @@ async function tryTerminateExecutionViaCdp(opts: {
|
||||
send("Target.attachToTarget", { targetId: opts.targetId, flatten: true }),
|
||||
1500,
|
||||
)) as { sessionId?: unknown };
|
||||
if (typeof attached?.sessionId === "string" && attached.sessionId.trim()) {
|
||||
sessionId = attached.sessionId;
|
||||
const attachedSessionId = normalizeOptionalString(attached?.sessionId);
|
||||
if (attachedSessionId) {
|
||||
sessionId = attachedSessionId;
|
||||
}
|
||||
}
|
||||
await runWithTimeout(send("Runtime.terminateExecution", undefined, sessionId), 1500);
|
||||
@@ -990,7 +994,7 @@ export async function forceDisconnectPlaywrightForTarget(opts: {
|
||||
|
||||
// Best-effort: kill any stuck JS to unblock the target's execution context before we
|
||||
// disconnect Playwright's CDP connection.
|
||||
const targetId = opts.targetId?.trim() || "";
|
||||
const targetId = normalizeOptionalString(opts.targetId) ?? "";
|
||||
if (targetId) {
|
||||
await tryTerminateExecutionViaCdp({ cdpUrl: normalized, targetId }).catch(() => {});
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
|
||||
import { formatErrorMessage } from "../infra/errors.js";
|
||||
import type { SsrFPolicy } from "../infra/net/ssrf.js";
|
||||
import type { BrowserActRequest, BrowserFormField } from "./client-actions-core.js";
|
||||
@@ -231,7 +232,7 @@ export async function pressKeyViaPlaywright(opts: {
|
||||
delayMs?: number;
|
||||
ssrfPolicy?: SsrFPolicy;
|
||||
}): Promise<void> {
|
||||
const key = String(opts.key ?? "").trim();
|
||||
const key = normalizeOptionalString(opts.key) ?? "";
|
||||
if (!key) {
|
||||
throw new Error("key is required");
|
||||
}
|
||||
@@ -336,7 +337,7 @@ export async function evaluateViaPlaywright(opts: {
|
||||
timeoutMs?: number;
|
||||
signal?: AbortSignal;
|
||||
}): Promise<unknown> {
|
||||
const fnText = String(opts.fn ?? "").trim();
|
||||
const fnText = normalizeOptionalString(opts.fn) ?? "";
|
||||
if (!fnText) {
|
||||
throw new Error("function is required");
|
||||
}
|
||||
@@ -512,13 +513,13 @@ export async function waitForViaPlaywright(opts: {
|
||||
});
|
||||
}
|
||||
if (opts.selector) {
|
||||
const selector = String(opts.selector).trim();
|
||||
const selector = normalizeOptionalString(opts.selector) ?? "";
|
||||
if (selector) {
|
||||
await page.locator(selector).first().waitFor({ state: "visible", timeout });
|
||||
}
|
||||
}
|
||||
if (opts.url) {
|
||||
const url = String(opts.url).trim();
|
||||
const url = normalizeOptionalString(opts.url) ?? "";
|
||||
if (url) {
|
||||
await page.waitForURL(url, { timeout });
|
||||
}
|
||||
@@ -527,7 +528,7 @@ export async function waitForViaPlaywright(opts: {
|
||||
await page.waitForLoadState(opts.loadState, { timeout });
|
||||
}
|
||||
if (opts.fn) {
|
||||
const fn = String(opts.fn).trim();
|
||||
const fn = normalizeOptionalString(opts.fn) ?? "";
|
||||
if (fn) {
|
||||
await page.waitForFunction(fn, { timeout });
|
||||
}
|
||||
@@ -709,8 +710,8 @@ export async function setInputFilesViaPlaywright(opts: {
|
||||
if (!opts.paths.length) {
|
||||
throw new Error("paths are required");
|
||||
}
|
||||
const inputRef = typeof opts.inputRef === "string" ? opts.inputRef.trim() : "";
|
||||
const element = typeof opts.element === "string" ? opts.element.trim() : "";
|
||||
const inputRef = normalizeOptionalString(opts.inputRef) ?? "";
|
||||
const element = normalizeOptionalString(opts.element) ?? "";
|
||||
if (inputRef && element) {
|
||||
throw new Error("inputRef and element are mutually exclusive");
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
|
||||
import { formatCliCommand } from "../cli/command-format.js";
|
||||
import { ensurePageState, getPageForTargetId } from "./pw-session.js";
|
||||
import { normalizeTimeoutMs } from "./pw-tools-core.shared.js";
|
||||
@@ -16,7 +17,7 @@ export async function responseBodyViaPlaywright(opts: {
|
||||
body: string;
|
||||
truncated?: boolean;
|
||||
}> {
|
||||
const pattern = String(opts.url ?? "").trim();
|
||||
const pattern = normalizeOptionalString(opts.url) ?? "";
|
||||
if (!pattern) {
|
||||
throw new Error("url is required");
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user