Compare commits

..

25 Commits

Author SHA1 Message Date
Peter Steinberger
188aff1b8b refactor: stage plugin enable-state resolution 2026-03-09 04:14:53 +00:00
Peter Steinberger
1b837a6b24 refactor: unify tool policy allow merging 2026-03-09 04:14:46 +00:00
Ayaan Zaidi
26e76f9a61 fix: dedupe inbound Telegram DM replies per agent (#40519)
Merged via squash.

Prepared head SHA: 6e235e7d1f
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Reviewed-by: @obviyus
2026-03-09 09:31:05 +05:30
Peter Steinberger
8befd88119 build(protocol): sync generated swift models 2026-03-09 03:49:50 +00:00
Peter Steinberger
99cbda83a2 fix(media): accept reader read result type 2026-03-09 03:49:50 +00:00
Peter Steinberger
e8775cda93 fix(agents): re-expose configured tools under restrictive profiles 2026-03-09 03:49:50 +00:00
Tak Hoffman
ef36cb8cbc chore(acpx): move runtime test fixtures to test-utils (openclaw#40548)
Verified:
- pnpm install --frozen-lockfile
- pnpm build
- pnpm check
- pnpm test:macmini
2026-03-08 22:47:04 -05:00
Ayaan Zaidi
f114a5c638 test: fix android talk config contract fixture 2026-03-09 09:15:49 +05:30
Kyle
a438ff4397 fix(plugin-sdk): remove remaining bundled plugin src imports (openclaw#39638)
Verified:
- pnpm build
- pnpm check
- pnpm test:macmini

Co-authored-by: Kyle <3477429+kyledh@users.noreply.github.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
2026-03-08 22:32:45 -05:00
Kesku
adec8b28bb alphabetize web search providers (#40259)
Merged via squash.

Prepared head SHA: be6350e5ae
Co-authored-by: kesku <62210496+kesku@users.noreply.github.com>
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Reviewed-by: @obviyus
2026-03-09 08:54:54 +05:30
Mariano
e3df94365b ACP: add optional ingress provenance receipts (#40473)
Merged via squash.

Prepared head SHA: b63e46dd94
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Reviewed-by: @mbelinky
2026-03-09 04:19:03 +01:00
Tyson Cung
4d501e4ccf fix(telegram): add download timeout to prevent polling loop hang (#40098)
Merged via squash.

Prepared head SHA: abdfa1a35f
Co-authored-by: tysoncung <45380903+tysoncung@users.noreply.github.com>
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Reviewed-by: @obviyus
2026-03-09 08:29:21 +05:30
yuweuii
f6243916b5 fix(models): use 1M context for openai-codex gpt-5.4 (#37876)
Merged via squash.

Prepared head SHA: c41020779e
Co-authored-by: yuweuii <82372187+yuweuii@users.noreply.github.com>
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Reviewed-by: @jalehman
2026-03-08 18:23:49 -07:00
Radek Sienkiewicz
b34158086a docs(changelog): correct Control UI contributor credit (#40420)
Merged via squash.

Prepared head SHA: e4295fe18b
Co-authored-by: velvet-shark <126378+velvet-shark@users.noreply.github.com>
Co-authored-by: velvet-shark <126378+velvet-shark@users.noreply.github.com>
Reviewed-by: @velvet-shark
2026-03-09 02:18:30 +01:00
Vincent Koc
eabda6e3a4 fix(tests): correct security check failure 2026-03-08 18:13:35 -07:00
Vincent Koc
6d5e142b93 Docker: improve build cache reuse (#40351)
* Docker: improve build cache reuse

* Tests: cover Docker build cache layout

* Docker: fix sandbox cache mount continuations

* Docker: document qr-import manifest scope

* Docker: narrow e2e install inputs

* CI: cache Docker builds in workflows

* CI: route sandbox smoke through setup script

* CI: keep sandbox smoke on script path
2026-03-08 17:57:46 -07:00
Radek Sienkiewicz
4f42c03a49 gateway: fix global Control UI 404s for symlinked wrappers and bundled package roots (#40385)
Merged via squash.

Prepared head SHA: 567b3ed684
Co-authored-by: velvet-shark <126378+velvet-shark@users.noreply.github.com>
Co-authored-by: velvet-shark <126378+velvet-shark@users.noreply.github.com>
Reviewed-by: @velvet-shark
2026-03-09 01:50:42 +01:00
Peter Steinberger
13bd3db307 chore(docs): drop refactor cleanup tracker 2026-03-09 00:26:20 +00:00
Peter Steinberger
ff4745fc3f refactor(models): split provider discovery helpers 2026-03-09 00:26:20 +00:00
Peter Steinberger
c29b098744 refactor(models): split models.json planning from writes 2026-03-09 00:26:20 +00:00
Peter Steinberger
24b53fcf47 refactor(agents): extract provider model normalization 2026-03-09 00:26:20 +00:00
Peter Steinberger
dfc18b7a2b refactor(models): extract list row builders 2026-03-09 00:26:20 +00:00
Peter Steinberger
141738f717 refactor: harden browser runtime profile handling 2026-03-09 00:25:43 +00:00
bbblending
4ff4ed7ec9 fix(config): refresh runtime snapshot from disk after write. Fixes #37175 (#37313)
Merged via squash.

Prepared head SHA: 69e1861abf
Co-authored-by: bbblending <122739024+bbblending@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
2026-03-08 19:49:15 -04:00
Peter Steinberger
362248e559 refactor: harden browser relay CDP flows 2026-03-08 23:46:10 +00:00
187 changed files with 1918 additions and 5790 deletions

View File

@@ -41,5 +41,3 @@ pattern = grep -q 'N[O]DE_COMPILE_CACHE=/var/tmp/openclaw-compile-cache' ~/.bash
pattern = env: \{ MISTRAL_API_K[E]Y: "sk-\.\.\." \},
pattern = "ap[i]Key": "xxxxx",
pattern = ap[i]Key: "A[I]za\.\.\.",
# Sparkle appcast signatures are release metadata, not credentials.
pattern = sparkle:edSignature="[A-Za-z0-9+/=]+"

View File

@@ -71,8 +71,6 @@ repos:
- 'ap[i]Key: "A[I]za\.\.\.",'
- --exclude-lines
- '"ap[i]Key": "(resolved|normalized|legacy)-key"(,)?'
- --exclude-lines
- 'sparkle:edSignature="[A-Za-z0-9+/=]+"'
# Shell script linting
- repo: https://github.com/koalaman/shellcheck-precommit
rev: v0.11.0

View File

@@ -153,8 +153,7 @@
"env: \\{ MISTRAL_API_K[E]Y: \"sk-\\.\\.\\.\" \\},",
"\"ap[i]Key\": \"xxxxx\"(,)?",
"ap[i]Key: \"A[I]za\\.\\.\\.\",",
"\"ap[i]Key\": \"(resolved|normalized|legacy)-key\"(,)?",
"sparkle:edSignature=\"[A-Za-z0-9+/=]+\""
"\"ap[i]Key\": \"(resolved|normalized|legacy)-key\"(,)?"
]
},
{
@@ -181,6 +180,29 @@
"line_number": 15
}
],
"appcast.xml": [
{
"type": "Base64 High Entropy String",
"filename": "appcast.xml",
"hashed_secret": "7afea670e53d801f1f881c99c40aa177e3395bfa",
"is_verified": false,
"line_number": 365
},
{
"type": "Base64 High Entropy String",
"filename": "appcast.xml",
"hashed_secret": "6e1ba26139ac4e73427e68a7eec2abf96bcf1fd4",
"is_verified": false,
"line_number": 584
},
{
"type": "Base64 High Entropy String",
"filename": "appcast.xml",
"hashed_secret": "c0baa9660a8d3b11874c63a535d8369f4a8fa8fa",
"is_verified": false,
"line_number": 723
}
],
"apps/android/app/src/test/java/ai/openclaw/android/node/AppUpdateHandlerTest.kt": [
{
"type": "Hex High Entropy String",
@@ -12911,14 +12933,14 @@
"filename": "src/telegram/monitor.test.ts",
"hashed_secret": "e5e9fa1ba31ecd1ae84f75caaa474f3a663f05f4",
"is_verified": false,
"line_number": 497
"line_number": 450
},
{
"type": "Secret Keyword",
"filename": "src/telegram/monitor.test.ts",
"hashed_secret": "5934c4d4a4fa5d66ddb3d3fc0bba84996c17a5b7",
"is_verified": false,
"line_number": 688
"line_number": 641
}
],
"src/telegram/webhook.test.ts": [
@@ -13013,5 +13035,5 @@
}
]
},
"generated_at": "2026-03-09T08:37:13Z"
"generated_at": "2026-03-09T01:11:58Z"
}

View File

@@ -48,4 +48,4 @@
--allman false
# Exclusions
--exclude .build,.swiftpm,DerivedData,node_modules,dist,coverage,xcuserdata,Peekaboo,Swabble,apps/android,apps/ios,apps/shared,apps/macos/Sources/MoltbotProtocol,apps/macos/Sources/OpenClaw/HostEnvSecurityPolicy.generated.swift
--exclude .build,.swiftpm,DerivedData,node_modules,dist,coverage,xcuserdata,Peekaboo,Swabble,apps/android,apps/ios,apps/shared,apps/macos/Sources/MoltbotProtocol

View File

@@ -19,8 +19,6 @@ excluded:
- "*.playground"
# Generated (protocol-gen-swift.ts)
- apps/macos/Sources/MoltbotProtocol/GatewayModels.swift
# Generated (generate-host-env-security-policy-swift.mjs)
- apps/macos/Sources/OpenClaw/HostEnvSecurityPolicy.generated.swift
analyzer_rules:
- unused_declaration

View File

@@ -2,87 +2,58 @@
Docs: https://docs.openclaw.ai
## Unreleased
### Changes
### Breaking
### Fixes
- macOS/LaunchAgent install: tighten LaunchAgent directory and plist permissions during install so launchd bootstrap does not fail when the target home path or generated plist inherited group/world-writable modes.
- Gateway/Control UI: keep dashboard auth tokens in session-scoped browser storage so same-tab refreshes preserve remote token auth without restoring long-lived localStorage token persistence, while scoping tokens to the selected gateway URL and fragment-only bootstrap flow. (#40892) thanks @velvet-shark.
- Models/Kimi Coding: send `anthropic-messages` tools in native Anthropic format again so `kimi-coding` stops degrading tool calls into XML/plain-text pseudo invocations instead of real `tool_use` blocks. (#38669, #39907, #40552) Thanks @opriz.
- Context engine/tests: add bundled-registry regression coverage for cross-chunk resolution, plugin-sdk re-exports, and concurrent chunk registration. (#40460) thanks @dsantoreis.
- Agents/embedded runner: bound compaction retry waiting and drain embedded runs during SIGUSR1 restart so session lanes recover instead of staying blocked behind compaction. (#40324) thanks @cgdusek.
- ACP/sessions.patch: allow `spawnedBy` and `spawnDepth` lineage fields on ACP session keys so `sessions_spawn` with `runtime: "acp"` no longer fails during child-session setup. Fixes #40971. (#40995) thanks @xaeon2026.
- ACP/stop reason mapping: resolve gateway chat `state: "error"` completions as ACP `end_turn` instead of `refusal` so transient backend failures are not surfaced as deliberate refusals. (#41187) thanks @pejmanjohn.
- ACP/setSessionMode: propagate gateway `sessions.patch` failures back to ACP clients so rejected mode changes no longer return silent success. (#41185) thanks @pejmanjohn.
## 2026.3.8
### Changes
- CLI/backup: add `openclaw backup create` and `openclaw backup verify` for local state archives, including `--only-config`, `--no-include-workspace`, manifest/payload validation, and backup guidance in destructive flows. (#40163) thanks @shichangs.
- macOS/onboarding: add a remote gateway token field for remote mode, preserve existing non-plaintext `gateway.remote.token` config values until explicitly replaced, and warn when the loaded token shape cannot be used directly from the macOS app. (#40187, supersedes #34614) Thanks @cgdusek.
- Talk mode: add top-level `talk.silenceTimeoutMs` config so Talk waits a configurable amount of silence before auto-sending the current transcript, while keeping each platform's existing default pause window when unset. (#39607) Thanks @danodoesdesign. Fixes #17147.
- Extensions/ACPX tests: move the shared runtime fixture helper from `src/runtime-internals/` to `src/test-utils/` so the test-only helper no longer looks like shipped runtime code.
- TUI: infer the active agent from the current workspace when launched inside a configured agent workspace, while preserving explicit `agent:` session targets. (#39591) thanks @arceus77-7.
- Tools/Brave web search: add opt-in `tools.web.search.brave.mode: "llm-context"` so `web_search` can call Brave's LLM Context endpoint and return extracted grounding snippets with source metadata, plus config/docs/test coverage. (#33383) Thanks @thirumaleshp.
- Talk mode: add top-level `talk.silenceTimeoutMs` config so Talk waits a configurable amount of silence before auto-sending the current transcript, while keeping each platform's existing default pause window when unset. (#39607) Thanks @danodoesdesign. Fixes #17147.
- CLI/install: include the short git commit hash in `openclaw --version` output when metadata is available, and keep installer version checks compatible with the decorated format. (#39712) thanks @sourman.
- Docs/Web search: restore $5/month free-credit details, replace defunct "Data for Search"/"Data for AI" plan names with current "Search" plan, and note legacy subscription validity in Brave setup docs. Follows up on #26860. (#40111) Thanks @remusao.
- macOS/onboarding: add a remote gateway token field for remote mode, preserve existing non-plaintext `gateway.remote.token` config values until explicitly replaced, and warn when the loaded token shape cannot be used directly from the macOS app. (#40187, supersedes #34614) Thanks @cgdusek.
- CLI/backup: add `openclaw backup create` and `openclaw backup verify` for local state archives, including `--only-config`, `--no-include-workspace`, manifest/payload validation, and backup guidance in destructive flows. (#40163) thanks @shichangs.
- CLI/backup: improve archive naming for date sorting, add config-only backup mode, and harden backup planning, publication, and verification edge cases. (#40163) Thanks @gumadeiras.
- ACP/Provenance: add optional ACP ingress provenance metadata and visible receipt injection (`openclaw acp --provenance off|meta|meta+receipt`) so OpenClaw agents can retain and report ACP-origin context with session trace IDs. (#40473) thanks @mbelinky.
- Tools/web search: alphabetize provider ordering across runtime selection, onboarding/configure pickers, and config metadata, so provider lists stay neutral and multi-key auto-detect now prefers Grok before Kimi. (#40259) thanks @kesku.
- Docs/Web search: restore $5/month free-credit details, replace defunct "Data for Search"/"Data for AI" plan names with current "Search" plan, and note legacy subscription validity in Brave setup docs. Follows up on #26860. (#40111) Thanks @remusao.
- Extensions/ACPX tests: move the shared runtime fixture helper from `src/runtime-internals/` to `src/test-utils/` so the test-only helper no longer looks like shipped runtime code.
### Breaking
### Fixes
- Update/macOS launchd restart: re-enable disabled LaunchAgent services before updater bootstrap so `openclaw update` can recover from a disabled gateway service instead of leaving the restart step stuck.
- macOS app/chat UI: route browser proxy through the local node browser service, preserve plain-text paste semantics, strip completed assistant trace/debug wrapper noise from transcripts, refresh permission state after returning from System Settings, and tolerate malformed cron rows in the macOS tab. (#39516) Thanks @Imhermes1.
- Android/Play distribution: remove self-update, background location, `screen.record`, and background mic capture from the Android app, narrow the foreground service to `dataSync` only, and clean up the legacy `location.enabledMode=always` preference migration. (#39660) Thanks @obviyus.
- Telegram/DM routing: dedupe inbound Telegram DMs per agent instead of per session key so the same DM cannot trigger duplicate replies when both `agent:main:main` and `agent:main:telegram:direct:<id>` resolve for one agent. Fixes #40005. Supersedes #40116. (#40519) thanks @obviyus.
- Cron/Telegram announce delivery: route text-only announce jobs through the real outbound adapters after finalizing descendant output so plain Telegram targets no longer report `delivered: true` when no message actually reached Telegram. (#40575) thanks @obviyus.
- Matrix/DM routing: add safer fallback detection for broken `m.direct` homeservers, honor explicit room bindings over DM classification, and preserve room-bound agent selection for Matrix DM rooms. (#19736) Thanks @derbronko.
- Feishu/plugin onboarding: clear the short-lived plugin discovery cache before reloading the registry after installing a channel plugin, so onboarding no longer re-prompts to download Feishu immediately after a successful install. Fixes #39642. (#39752) Thanks @GazeKingNuWu.
- Docker/runtime image: prune dev dependencies, strip build-only dist metadata for smaller Docker images. (#40307) Thanks @vincentkoc.
- Plugins/channel onboarding: prefer bundled channel plugins over duplicate npm-installed copies during onboarding and release-channel sync, preventing bundled plugins from being shadowed by npm installs with the same plugin ID. (#40092)
- Config/runtime snapshots: keep secrets-runtime-resolved config and auth-profile snapshots intact after config writes so follow-up reads still see file-backed secret values while picking up the persisted config update. (#37313) thanks @bbblending.
- Gateway/Control UI: resolve bundled dashboard assets through symlinked global wrappers and auto-detected package roots, while keeping configured and custom roots on the strict hardlink boundary. (#40385) Thanks @LarytheLord.
- Browser/extension relay: add `browser.relayBindHost` so the Chrome relay can bind to an explicit non-loopback address for WSL2 and other cross-namespace setups, while preserving loopback-only defaults. (#39364) Thanks @mvanhorn.
- Browser/CDP: normalize loopback direct WebSocket CDP URLs back to HTTP(S) for `/json/*` tab operations so local `ws://` / `wss://` profiles can still list, focus, open, and close tabs after the new direct-WS support lands. (#31085) Thanks @shrey150.
- Browser/CDP: rewrite wildcard `ws://0.0.0.0` and `ws://[::]` debugger URLs from remote `/json/version` responses back to the external CDP host/port, fixing Browserless-style container endpoints. (#17760) Thanks @joeharouni.
- Browser/extension relay: wait briefly for a previously attached Chrome tab to reappear after transient relay drops before failing with `tab not found`, reducing noisy reconnect flakes. (#32461) Thanks @AaronWander.
- macOS/Tailscale gateway discovery: keep Tailscale Serve probing alive when other remote gateways are already discovered, prefer direct transport for resolved `.ts.net` and Tailscale Serve gateways, and set `TERM=dumb` for GUI-launched Tailscale CLI discovery. (#40167) thanks @ngutman.
- TUI/theme: detect light terminal backgrounds via `COLORFGBG` and pick a WCAG AA-compliant light palette, with `OPENCLAW_THEME=light|dark` override for terminals without auto-detection. (#38636) Thanks @ademczuk and @vincentkoc.
- Agents/openai-codex: normalize `gpt-5.4` fallback transport back to `openai-codex-responses` on `chatgpt.com/backend-api` when config drifts to the generic OpenAI responses endpoint. (#38736) Thanks @0xsline.
- Models/openai-codex GPT-5.4 forward-compat: use the GPT-5.4 1,050,000-token context window and 128,000 max tokens for `openai-codex/gpt-5.4` instead of inheriting stale legacy Codex limits in resolver fallbacks and model listing. (#37876) thanks @yuweuii.
- 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.
- Agents/failover: detect Amazon Bedrock `Too many tokens per day` quota errors as rate limits across fallback, cron retry, and memory embeddings while keeping context-window `too many tokens per request` errors out of the rate-limit lane. (#39377) Thanks @gambletan.
- macOS app/chat UI: route browser proxy through the local node browser service, preserve plain-text paste semantics, strip completed assistant trace/debug wrapper noise from transcripts, refresh permission state after returning from System Settings, and tolerate malformed cron rows in the macOS tab. (#39516) Thanks @Imhermes1.
- Mattermost replies: keep `root_id` pinned to the existing thread root when an agent replies inside a thread, while still using reply-target threading for top-level posts. (#27744) thanks @hnykda.
- Agents/failover: detect Amazon Bedrock `Too many tokens per day` quota errors as rate limits across fallback, cron retry, and memory embeddings while keeping context-window `too many tokens per request` errors out of the rate-limit lane. (#39377) Thanks @gambletan.
- Android/Play distribution: remove self-update, background location, `screen.record`, and background mic capture from the Android app, narrow the foreground service to `dataSync` only, and clean up the legacy `location.enabledMode=always` preference migration. (#39660) Thanks @obviyus.
- Telegram/DM partial streaming: keep DM preview lanes on real message edits instead of native draft materialization so final replies no longer flash a second duplicate copy before collapsing back to one.
- 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.
- 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.
- Agents/openai-codex: normalize `gpt-5.4` fallback transport back to `openai-codex-responses` on `chatgpt.com/backend-api` when config drifts to the generic OpenAI responses endpoint. (#38736) Thanks @0xsline.
- Browser/CDP: normalize loopback direct WebSocket CDP URLs back to HTTP(S) for `/json/*` tab operations so local `ws://` / `wss://` profiles can still list, focus, open, and close tabs after the new direct-WS support lands. (#31085) Thanks @shrey150.
- Browser/CDP: rewrite wildcard `ws://0.0.0.0` and `ws://[::]` debugger URLs from remote `/json/version` responses back to the external CDP host/port, fixing Browserless-style container endpoints. (#17760) Thanks @joeharouni.
- Browser/extension relay: wait briefly for a previously attached Chrome tab to reappear after transient relay drops before failing with `tab not found`, reducing noisy reconnect flakes. (#32461) Thanks @AaronWander.
- Browser/extension relay: add `browser.relayBindHost` so the Chrome relay can bind to an explicit non-loopback address for WSL2 and other cross-namespace setups, while preserving loopback-only defaults. (#39364) Thanks @mvanhorn.
- Docs/browser: add a layered WSL2 + Windows remote Chrome CDP troubleshooting guide, including Control UI origin pitfalls and extension-relay bind-address guidance. (#39407) Thanks @Owlock.
- Context engine registry/bundled builds: share the registry state through a `globalThis` singleton so duplicated bundled module copies can resolve engines registered by each other at runtime, with regression coverage for duplicate-module imports. (#40115) thanks @jalehman.
- macOS/Tailscale gateway discovery: keep Tailscale Serve probing alive when other remote gateways are already discovered, prefer direct transport for resolved `.ts.net` and Tailscale Serve gateways, and set `TERM=dumb` for GUI-launched Tailscale CLI discovery. (#40167) thanks @ngutman.
- Podman/setup: fix `cannot chdir: Permission denied` in `run_as_user` when `setup-podman.sh` is invoked from a directory the target user cannot access, by wrapping user-switch calls in a subshell that cd's to `/tmp` with `/` fallback. (#39435) Thanks @langdon and @jlcbk.
- Podman/SELinux: auto-detect SELinux enforcing/permissive mode and add `:Z` relabel to bind mounts in `run-openclaw-podman.sh` and the Quadlet template, fixing `EACCES` on Fedora/RHEL hosts. Supports `OPENCLAW_BIND_MOUNT_OPTIONS` override. (#39449) Thanks @langdon and @githubbzxs.
- TUI/theme: detect light terminal backgrounds via `COLORFGBG` and pick a WCAG AA-compliant light palette, with `OPENCLAW_THEME=light|dark` override for terminals without auto-detection. (#38636) Thanks @ademczuk and @vincentkoc.
- Agents/context-engine plugins: bootstrap runtime plugins once at embedded-run, compaction, and subagent boundaries so plugin-provided context engines and hooks load from the active workspace before runtime resolution. (#40232)
- Config/runtime snapshots: keep secrets-runtime-resolved config and auth-profile snapshots intact after config writes so follow-up reads still see file-backed secret values while picking up the persisted config update. (#37313) thanks @bbblending.
- Gateway/Control UI: resolve bundled dashboard assets through symlinked global wrappers and auto-detected package roots, while keeping configured and custom roots on the strict hardlink boundary. (#40385) Thanks @LarytheLord.
- Docs/Changelog: correct the contributor credit for the bundled Control UI global-install fix to @LarytheLord. (#40420) Thanks @velvet-shark.
- Models/openai-codex GPT-5.4 forward-compat: use the GPT-5.4 1,050,000-token context window and 128,000 max tokens for `openai-codex/gpt-5.4` instead of inheriting stale legacy Codex limits in resolver fallbacks and model listing. (#37876) thanks @yuweuii.
- Telegram/media downloads: time out only stalled body reads so polling recovers from hung file downloads without aborting slow downloads that are still streaming data. (#40098) thanks @tysoncung.
- Docker/runtime image: prune dev dependencies, strip build-only dist metadata for smaller Docker images. (#40307) Thanks @vincentkoc.
- Gateway/restart timeout recovery: exit non-zero when restart-triggered shutdown drains time out so launchd/systemd restart the gateway instead of treating the failed restart as a clean stop. Landed from contributor PR #40380 by @dsantoreis. Thanks @dsantoreis.
- Gateway/config restart guard: validate config before service start/restart and keep post-SIGUSR1 startup failures from crashing the gateway process, reducing invalid-config restart loops and macOS permission loss. Landed from contributor PR #38699 by @lml2468. Thanks @lml2468.
- Gateway/launchd respawn detection: treat `XPC_SERVICE_NAME` as a launchd supervision hint so macOS restarts exit cleanly under launchd instead of attempting detached self-respawn. Landed from contributor PR #20555 by @dimat. Thanks @dimat.
- Telegram/poll restart cleanup: abort the in-flight Telegram API fetch when shutdown or forced polling restarts stop a runner, preventing stale `getUpdates` long polls from colliding with the replacement runner. Landed from contributor PR #23950 by @Gkinthecodeland. Thanks @Gkinthecodeland.
- Cron/restart catch-up staggering: limit immediate missed-job replay on startup and reschedule the deferred remainder from the post-catchup clock so restart bursts do not starve the gateway or silently skip overdue recurring jobs. Landed from contributor PR #18925 by @rexlunae. Thanks @rexlunae.
- Cron/owner-only tools: pass trusted isolated cron runs into the embedded agent with owner context so `cron`/`gateway` tooling remains available after the owner-auth hardening narrowed direct-message ownership inference.
- Browser/SSRF: block private-network intermediate redirect hops in strict browser navigation flows and fail closed when remote tab-open paths cannot inspect redirect chains. Thanks @zpbrent.
- MS Teams/authz: keep `groupPolicy: "allowlist"` enforcing sender allowlists even when a team/channel route allowlist is configured, so route matches no longer widen group access to every sender in that route. Thanks @zpbrent.
- Security/system.run: bind approved `bun` and `deno run` script operands to on-disk file snapshots so post-approval script rewrites are denied before execution.
- Skills/download installs: pin the validated per-skill tools root before writing downloaded archives, so rebinding the lexical tools path cannot redirect download writes outside the intended tools directory. Thanks @tdjackey.
- Telegram/DM routing: dedupe inbound Telegram DMs per agent instead of per session key so the same DM cannot trigger duplicate replies when both `agent:main:main` and `agent:main:telegram:direct:<id>` resolve for one agent. Fixes #40005. Supersedes #40116. (#40519) thanks @obviyus.
## 2026.3.7

View File

@@ -57,21 +57,9 @@ Welcome to the lobster tank! 🦞
- GitHub: [@joshavant](https://github.com/joshavant) · X: [@joshavant](https://x.com/joshavant)
- **Jonathan Taylor** - ACP subsystem, Gateway features/bugs, Gog/Mog/Sog CLI's, SEDMAT
- GitHub [@visionik](https://github.com/visionik) · X: [@visionik](https://x.com/visionik)
- Github [@visionik](https://github.com/visionik) · X: [@visionik](https://x.com/visionik)
- **Josh Lehman** - Compaction, Tlon/Urbit subsystem
- GitHub [@jalehman](https://github.com/jalehman) · X: [@jlehman\_](https://x.com/jlehman_)
- **Radek Sienkiewicz** - Control UI + WebChat correctness
- GitHub [@velvet-shark](https://github.com/velvet-shark) · X: [@velvet_shark](https://twitter.com/velvet_shark)
- **Muhammed Mukhthar** - Mattermost, CLI
- GitHub [@mukhtharcm](https://github.com/mukhtharcm) · X: [@mukhtharcm](https://x.com/mukhtharcm)
- **Altay** - Agents, CLI, error handling
- GitHub [@altaywtf](https://github.com/altaywtf) · X: [@altaywtf](https://x.com/altaywtf)
- **Robin Waslander** - Security, PR triage, bug fixes
- GitHub: [@hydro13](https://github.com/hydro13) · X: [@Robin_waslander](https://x.com/Robin_waslander)
- Github [@jalehman](https://github.com/jalehman) · X: [@jlehman\_](https://x.com/jlehman_)
## How to Contribute

View File

@@ -2,80 +2,6 @@
<rss xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" version="2.0">
<channel>
<title>OpenClaw</title>
<item>
<title>2026.3.8-beta.1</title>
<pubDate>Mon, 09 Mar 2026 07:19:57 +0000</pubDate>
<link>https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml</link>
<sparkle:version>2026030801</sparkle:version>
<sparkle:shortVersionString>2026.3.8-beta.1</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
<description><![CDATA[<h2>OpenClaw 2026.3.8-beta.1</h2>
<h3>Changes</h3>
<ul>
<li>CLI/backup: add <code>openclaw backup create</code> and <code>openclaw backup verify</code> for local state archives, including <code>--only-config</code>, <code>--no-include-workspace</code>, manifest/payload validation, and backup guidance in destructive flows. (#40163) thanks @shichangs.</li>
<li>macOS/onboarding: add a remote gateway token field for remote mode, preserve existing non-plaintext <code>gateway.remote.token</code> config values until explicitly replaced, and warn when the loaded token shape cannot be used directly from the macOS app. (#40187, supersedes #34614) Thanks @cgdusek.</li>
<li>Talk mode: add top-level <code>talk.silenceTimeoutMs</code> config so Talk waits a configurable amount of silence before auto-sending the current transcript, while keeping each platform's existing default pause window when unset. (#39607) Thanks @danodoesdesign. Fixes #17147.</li>
<li>TUI: infer the active agent from the current workspace when launched inside a configured agent workspace, while preserving explicit <code>agent:</code> session targets. (#39591) thanks @arceus77-7.</li>
<li>Tools/Brave web search: add opt-in <code>tools.web.search.brave.mode: "llm-context"</code> so <code>web_search</code> can call Brave's LLM Context endpoint and return extracted grounding snippets with source metadata, plus config/docs/test coverage. (#33383) Thanks @thirumaleshp.</li>
<li>CLI/install: include the short git commit hash in <code>openclaw --version</code> output when metadata is available, and keep installer version checks compatible with the decorated format. (#39712) thanks @sourman.</li>
<li>CLI/backup: improve archive naming for date sorting, add config-only backup mode, and harden backup planning, publication, and verification edge cases. (#40163) Thanks @gumadeiras.</li>
<li>ACP/Provenance: add optional ACP ingress provenance metadata and visible receipt injection (<code>openclaw acp --provenance off|meta|meta+receipt</code>) so OpenClaw agents can retain and report ACP-origin context with session trace IDs. (#40473) thanks @mbelinky.</li>
<li>Tools/web search: alphabetize provider ordering across runtime selection, onboarding/configure pickers, and config metadata, so provider lists stay neutral and multi-key auto-detect now prefers Grok before Kimi. (#40259) thanks @kesku.</li>
<li>Docs/Web search: restore $5/month free-credit details, replace defunct "Data for Search"/"Data for AI" plan names with current "Search" plan, and note legacy subscription validity in Brave setup docs. Follows up on #26860. (#40111) Thanks @remusao.</li>
<li>Extensions/ACPX tests: move the shared runtime fixture helper from <code>src/runtime-internals/</code> to <code>src/test-utils/</code> so the test-only helper no longer looks like shipped runtime code.</li>
</ul>
<h3>Fixes</h3>
<ul>
<li>macOS app/chat UI: route browser proxy through the local node browser service, preserve plain-text paste semantics, strip completed assistant trace/debug wrapper noise from transcripts, refresh permission state after returning from System Settings, and tolerate malformed cron rows in the macOS tab. (#39516) Thanks @Imhermes1.</li>
<li>Android/Play distribution: remove self-update, background location, <code>screen.record</code>, and background mic capture from the Android app, narrow the foreground service to <code>dataSync</code> only, and clean up the legacy <code>location.enabledMode=always</code> preference migration. (#39660) Thanks @obviyus.</li>
<li>Telegram/DM routing: dedupe inbound Telegram DMs per agent instead of per session key so the same DM cannot trigger duplicate replies when both <code>agent:main:main</code> and <code>agent:main:telegram:direct:<id></code> resolve for one agent. Fixes #40005. Supersedes #40116. (#40519) thanks @obviyus.</li>
<li>Cron/Telegram announce delivery: route text-only announce jobs through the real outbound adapters after finalizing descendant output so plain Telegram targets no longer report <code>delivered: true</code> when no message actually reached Telegram. (#40575) thanks @obviyus.</li>
<li>Matrix/DM routing: add safer fallback detection for broken <code>m.direct</code> homeservers, honor explicit room bindings over DM classification, and preserve room-bound agent selection for Matrix DM rooms. (#19736) Thanks @derbronko.</li>
<li>Feishu/plugin onboarding: clear the short-lived plugin discovery cache before reloading the registry after installing a channel plugin, so onboarding no longer re-prompts to download Feishu immediately after a successful install. Fixes #39642. (#39752) Thanks @GazeKingNuWu.</li>
<li>Plugins/channel onboarding: prefer bundled channel plugins over duplicate npm-installed copies during onboarding and release-channel sync, preventing bundled plugins from being shadowed by npm installs with the same plugin ID. (#40092)</li>
<li>Config/runtime snapshots: keep secrets-runtime-resolved config and auth-profile snapshots intact after config writes so follow-up reads still see file-backed secret values while picking up the persisted config update. (#37313) thanks @bbblending.</li>
<li>Gateway/Control UI: resolve bundled dashboard assets through symlinked global wrappers and auto-detected package roots, while keeping configured and custom roots on the strict hardlink boundary. (#40385) Thanks @LarytheLord.</li>
<li>Browser/extension relay: add <code>browser.relayBindHost</code> so the Chrome relay can bind to an explicit non-loopback address for WSL2 and other cross-namespace setups, while preserving loopback-only defaults. (#39364) Thanks @mvanhorn.</li>
<li>Browser/CDP: normalize loopback direct WebSocket CDP URLs back to HTTP(S) for <code>/json/*</code> tab operations so local <code>ws://</code> / <code>wss://</code> profiles can still list, focus, open, and close tabs after the new direct-WS support lands. (#31085) Thanks @shrey150.</li>
<li>Browser/CDP: rewrite wildcard <code>ws://0.0.0.0</code> and <code>ws://[::]</code> debugger URLs from remote <code>/json/version</code> responses back to the external CDP host/port, fixing Browserless-style container endpoints. (#17760) Thanks @joeharouni.</li>
<li>Browser/extension relay: wait briefly for a previously attached Chrome tab to reappear after transient relay drops before failing with <code>tab not found</code>, reducing noisy reconnect flakes. (#32461) Thanks @AaronWander.</li>
<li>macOS/Tailscale gateway discovery: keep Tailscale Serve probing alive when other remote gateways are already discovered, prefer direct transport for resolved <code>.ts.net</code> and Tailscale Serve gateways, and set <code>TERM=dumb</code> for GUI-launched Tailscale CLI discovery. (#40167) thanks @ngutman.</li>
<li>TUI/theme: detect light terminal backgrounds via <code>COLORFGBG</code> and pick a WCAG AA-compliant light palette, with <code>OPENCLAW_THEME=light|dark</code> override for terminals without auto-detection. (#38636) Thanks @ademczuk and @vincentkoc.</li>
<li>Agents/openai-codex: normalize <code>gpt-5.4</code> fallback transport back to <code>openai-codex-responses</code> on <code>chatgpt.com/backend-api</code> when config drifts to the generic OpenAI responses endpoint. (#38736) Thanks @0xsline.</li>
<li>Models/openai-codex GPT-5.4 forward-compat: use the GPT-5.4 1,050,000-token context window and 128,000 max tokens for <code>openai-codex/gpt-5.4</code> instead of inheriting stale legacy Codex limits in resolver fallbacks and model listing. (#37876) thanks @yuweuii.</li>
<li>Tools/web search: restore Perplexity OpenRouter/Sonar compatibility for legacy <code>OPENROUTER_API_KEY</code>, <code>sk-or-...</code>, and explicit <code>perplexity.baseUrl</code> / <code>model</code> setups while keeping direct Perplexity keys on the native Search API path. (#39937) Thanks @obviyus.</li>
<li>Agents/failover: detect Amazon Bedrock <code>Too many tokens per day</code> quota errors as rate limits across fallback, cron retry, and memory embeddings while keeping context-window <code>too many tokens per request</code> errors out of the rate-limit lane. (#39377) Thanks @gambletan.</li>
<li>Mattermost replies: keep <code>root_id</code> pinned to the existing thread root when an agent replies inside a thread, while still using reply-target threading for top-level posts. (#27744) thanks @hnykda.</li>
<li>Telegram/DM partial streaming: keep DM preview lanes on real message edits instead of native draft materialization so final replies no longer flash a second duplicate copy before collapsing back to one.</li>
<li>macOS overlays: fix VoiceWake, Talk, and Notify overlay exclusivity crashes by removing shared <code>inout</code> visibility mutation from <code>OverlayPanelFactory.present</code>, and add a repeated Talk overlay smoke test. (#39275, #39321) Thanks @fellanH.</li>
<li>macOS Talk Mode: set the speech recognition request <code>taskHint</code> to <code>.dictation</code> for mic capture, and add regression coverage for the request defaults. (#38445) Thanks @dmiv.</li>
<li>macOS release packaging: default <code>scripts/package-mac-app.sh</code> to universal binaries for <code>BUILD_CONFIG=release</code>, and clarify that <code>scripts/package-mac-dist.sh</code> already produces the release zip + DMG. (#33891) Thanks @cgdusek.</li>
<li>Hooks/session-memory: keep <code>/new</code> and <code>/reset</code> 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.</li>
<li>Sessions/model switch: clear stale cached <code>contextTokens</code> when a session changes models so status and runtime paths recompute against the active model window. (#38044) thanks @yuweuii.</li>
<li>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.</li>
<li>Docs/browser: add a layered WSL2 + Windows remote Chrome CDP troubleshooting guide, including Control UI origin pitfalls and extension-relay bind-address guidance. (#39407) Thanks @Owlock.</li>
<li>Context engine registry/bundled builds: share the registry state through a <code>globalThis</code> singleton so duplicated bundled module copies can resolve engines registered by each other at runtime, with regression coverage for duplicate-module imports. (#40115) thanks @jalehman.</li>
<li>Podman/setup: fix <code>cannot chdir: Permission denied</code> in <code>run_as_user</code> when <code>setup-podman.sh</code> is invoked from a directory the target user cannot access, by wrapping user-switch calls in a subshell that cd's to <code>/tmp</code> with <code>/</code> fallback. (#39435) Thanks @langdon and @jlcbk.</li>
<li>Podman/SELinux: auto-detect SELinux enforcing/permissive mode and add <code>:Z</code> relabel to bind mounts in <code>run-openclaw-podman.sh</code> and the Quadlet template, fixing <code>EACCES</code> on Fedora/RHEL hosts. Supports <code>OPENCLAW_BIND_MOUNT_OPTIONS</code> override. (#39449) Thanks @langdon and @githubbzxs.</li>
<li>Agents/context-engine plugins: bootstrap runtime plugins once at embedded-run, compaction, and subagent boundaries so plugin-provided context engines and hooks load from the active workspace before runtime resolution. (#40232)</li>
<li>Docs/Changelog: correct the contributor credit for the bundled Control UI global-install fix to @LarytheLord. (#40420) Thanks @velvet-shark.</li>
<li>Telegram/media downloads: time out only stalled body reads so polling recovers from hung file downloads without aborting slow downloads that are still streaming data. (#40098) thanks @tysoncung.</li>
<li>Docker/runtime image: prune dev dependencies, strip build-only dist metadata for smaller Docker images. (#40307) Thanks @vincentkoc.</li>
<li>Gateway/restart timeout recovery: exit non-zero when restart-triggered shutdown drains time out so launchd/systemd restart the gateway instead of treating the failed restart as a clean stop. Landed from contributor PR #40380 by @dsantoreis. Thanks @dsantoreis.</li>
<li>Gateway/config restart guard: validate config before service start/restart and keep post-SIGUSR1 startup failures from crashing the gateway process, reducing invalid-config restart loops and macOS permission loss. Landed from contributor PR #38699 by @lml2468. Thanks @lml2468.</li>
<li>Gateway/launchd respawn detection: treat <code>XPC_SERVICE_NAME</code> as a launchd supervision hint so macOS restarts exit cleanly under launchd instead of attempting detached self-respawn. Landed from contributor PR #20555 by @dimat. Thanks @dimat.</li>
<li>Telegram/poll restart cleanup: abort the in-flight Telegram API fetch when shutdown or forced polling restarts stop a runner, preventing stale <code>getUpdates</code> long polls from colliding with the replacement runner. Landed from contributor PR #23950 by @Gkinthecodeland. Thanks @Gkinthecodeland.</li>
<li>Cron/restart catch-up staggering: limit immediate missed-job replay on startup and reschedule the deferred remainder from the post-catchup clock so restart bursts do not starve the gateway or silently skip overdue recurring jobs. Landed from contributor PR #18925 by @rexlunae. Thanks @rexlunae.</li>
<li>Cron/owner-only tools: pass trusted isolated cron runs into the embedded agent with owner context so <code>cron</code>/<code>gateway</code> tooling remains available after the owner-auth hardening narrowed direct-message ownership inference.</li>
<li>Browser/SSRF: block private-network intermediate redirect hops in strict browser navigation flows and fail closed when remote tab-open paths cannot inspect redirect chains. Thanks @zpbrent.</li>
<li>MS Teams/authz: keep <code>groupPolicy: "allowlist"</code> enforcing sender allowlists even when a team/channel route allowlist is configured, so route matches no longer widen group access to every sender in that route. Thanks @zpbrent.</li>
<li>Security/system.run: bind approved <code>bun</code> and <code>deno run</code> script operands to on-disk file snapshots so post-approval script rewrites are denied before execution.</li>
<li>Skills/download installs: pin the validated per-skill tools root before writing downloaded archives, so rebinding the lexical tools path cannot redirect download writes outside the intended tools directory. Thanks @tdjackey.</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.3.8-beta.1/OpenClaw-2026.3.8-beta.1.zip" length="23407015" type="application/octet-stream" sparkle:edSignature="KCqhSmu4b0tHf55RqcQOHorsc55CgBI5BUmK/NTizxNq04INn/7QvsamHYQou9DbB2IW6B2nawBC4nn4au5yDA=="/>
</item>
<item>
<title>2026.3.7</title>
<pubDate>Sun, 08 Mar 2026 04:42:35 +0000</pubDate>
@@ -658,5 +584,144 @@
<enclosure url="https://github.com/openclaw/openclaw/releases/download/v2026.3.2/OpenClaw-2026.3.2.zip" length="23181513" type="application/octet-stream" sparkle:edSignature="THMgkcoMgz2vv5zse3Po3K7l3Or2RhBKurXZIi8iYVXN76yJy1YXAY6kXi6ovD+dbYn68JKYDIKA1Ya78bO7BQ=="/>
<!-- pragma: allowlist secret -->
</item>
<item>
<title>2026.3.1</title>
<pubDate>Mon, 02 Mar 2026 04:40:59 +0000</pubDate>
<link>https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml</link>
<sparkle:version>2026030190</sparkle:version>
<sparkle:shortVersionString>2026.3.1</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
<description><![CDATA[<h2>OpenClaw 2026.3.1</h2>
<h3>Changes</h3>
<ul>
<li>Agents/Thinking defaults: set <code>adaptive</code> as the default thinking level for Anthropic Claude 4.6 models (including Bedrock Claude 4.6 refs) while keeping other reasoning-capable models at <code>low</code> unless explicitly configured.</li>
<li>Gateway/Container probes: add built-in HTTP liveness/readiness endpoints (<code>/health</code>, <code>/healthz</code>, <code>/ready</code>, <code>/readyz</code>) for Docker/Kubernetes health checks, with fallback routing so existing handlers on those paths are not shadowed. (#31272) Thanks @vincentkoc.</li>
<li>Android/Nodes: add <code>camera.list</code>, <code>device.permissions</code>, <code>device.health</code>, and <code>notifications.actions</code> (<code>open</code>/<code>dismiss</code>/<code>reply</code>) on Android nodes, plus first-class node-tool actions for the new device/notification commands. (#28260) Thanks @obviyus.</li>
<li>Discord/Thread bindings: replace fixed TTL lifecycle with inactivity (<code>idleHours</code>, default 24h) plus optional hard <code>maxAgeHours</code> lifecycle controls, and add <code>/session idle</code> + <code>/session max-age</code> commands for focused thread-bound sessions. (#27845) Thanks @osolmaz.</li>
<li>Telegram/DM topics: add per-DM <code>direct</code> + topic config (allowlists, <code>dmPolicy</code>, <code>skills</code>, <code>systemPrompt</code>, <code>requireTopic</code>), route DM topics as distinct inbound/outbound sessions, and enforce topic-aware authorization/debounce for messages, callbacks, commands, and reactions. Landed from contributor PR #30579 by @kesor. Thanks @kesor.</li>
<li>Web UI/Cron i18n: localize cron page labels, filters, form help text, and validation/error messaging in English and zh-CN. (#29315) Thanks @BUGKillerKing.</li>
<li>OpenAI/Streaming transport: make <code>openai</code> Responses WebSocket-first by default (<code>transport: "auto"</code> with SSE fallback), add shared OpenAI WS stream/connection runtime wiring with per-session cleanup, and preserve server-side compaction payload mutation (<code>store</code> + <code>context_management</code>) on the WS path.</li>
<li>Android/Gateway capability refresh: add live Android capability integration coverage and node canvas capability refresh wiring, plus runtime hardening for A2UI readiness retries, scoped canvas URL normalization, debug diagnostics JSON, and JavaScript MIME delivery. (#28388) Thanks @obviyus.</li>
<li>Android/Nodes parity: add <code>system.notify</code>, <code>photos.latest</code>, <code>contacts.search</code>/<code>contacts.add</code>, <code>calendar.events</code>/<code>calendar.add</code>, and <code>motion.activity</code>/<code>motion.pedometer</code>, with motion sensor-aware command gating and improved activity sampling reliability. (#29398) Thanks @obviyus.</li>
<li>CLI/Config: add <code>openclaw config file</code> to print the active config file path resolved from <code>OPENCLAW_CONFIG_PATH</code> or the default location. (#26256) thanks @cyb1278588254.</li>
<li>Feishu/Docx tables + uploads: add <code>feishu_doc</code> actions for Docx table creation/cell writing (<code>create_table</code>, <code>write_table_cells</code>, <code>create_table_with_values</code>) and image/file uploads (<code>upload_image</code>, <code>upload_file</code>) with stricter create/upload error handling for missing <code>document_id</code> and placeholder cleanup failures. (#20304) Thanks @xuhao1.</li>
<li>Feishu/Reactions: add inbound <code>im.message.reaction.created_v1</code> handling, route verified reactions through synthetic inbound turns, and harden verification with timeout + fail-closed filtering so non-bot or unverified reactions are dropped. (#16716) Thanks @schumilin.</li>
<li>Feishu/Chat tooling: add <code>feishu_chat</code> tool actions for chat info and member queries, with configurable enablement under <code>channels.feishu.tools.chat</code>. (#14674) Thanks @liuweifly.</li>
<li>Feishu/Doc permissions: support optional owner permission grant fields on <code>feishu_doc</code> create and report permission metadata only when the grant call succeeds, with regression coverage for success/failure/omitted-owner paths. (#28295) Thanks @zhoulongchao77.</li>
<li>Web UI/i18n: add German (<code>de</code>) locale support and auto-render language options from supported locale constants in Overview settings. (#28495) thanks @dsantoreis.</li>
<li>Tools/Diffs: add a new optional <code>diffs</code> plugin tool for read-only diff rendering from before/after text or unified patches, with gateway viewer URLs for canvas and PNG image output. Thanks @gumadeiras.</li>
<li>Memory/LanceDB: support custom OpenAI <code>baseUrl</code> and embedding dimensions for LanceDB memory. (#17874) Thanks @rish2jain and @vincentkoc.</li>
<li>ACP/ACPX streaming: pin ACPX plugin support to <code>0.1.15</code>, add configurable ACPX command/version probing, and streamline ACP stream delivery (<code>final_only</code> default + reduced tool-event noise) with matching runtime and test updates. (#30036) Thanks @osolmaz.</li>
<li>Shell env markers: set <code>OPENCLAW_SHELL</code> across shell-like runtimes (<code>exec</code>, <code>acp</code>, <code>acp-client</code>, <code>tui-local</code>) so shell startup/config rules can target OpenClaw contexts consistently, and document the markers in env/exec/acp/TUI docs. Thanks @vincentkoc.</li>
<li>Cron/Heartbeat light bootstrap context: add opt-in lightweight bootstrap mode for automation runs (<code>--light-context</code> for cron agent turns and <code>agents.*.heartbeat.lightContext</code> for heartbeat), keeping only <code>HEARTBEAT.md</code> for heartbeat runs and skipping bootstrap-file injection for cron lightweight runs. (#26064) Thanks @jose-velez.</li>
<li>OpenAI/WebSocket warm-up: add optional OpenAI Responses WebSocket warm-up (<code>response.create</code> with <code>generate:false</code>), enable it by default for <code>openai/*</code>, and expose <code>params.openaiWsWarmup</code> for per-model enable/disable control.</li>
<li>Agents/Subagents runtime events: replace ad-hoc subagent completion system-message handoff with typed internal completion events (<code>task_completion</code>) that are rendered consistently across direct and queued announce paths, with gateway/CLI plumbing for structured <code>internalEvents</code>.</li>
</ul>
<h3>Breaking</h3>
<ul>
<li><strong>BREAKING:</strong> Node exec approval payloads now require <code>systemRunPlan</code>. <code>host=node</code> approval requests without that plan are rejected.</li>
<li><strong>BREAKING:</strong> Node <code>system.run</code> execution now pins path-token commands to the canonical executable path (<code>realpath</code>) in both allowlist and approval execution flows. Integrations/tests that asserted token-form argv (for example <code>tr</code>) must now accept canonical paths (for example <code>/usr/bin/tr</code>).</li>
</ul>
<h3>Fixes</h3>
<ul>
<li>Android/Nodes reliability: reject <code>facing=both</code> when <code>deviceId</code> is set to avoid mislabeled duplicate captures, allow notification <code>open</code>/<code>reply</code> on non-clearable entries while still gating dismiss, trigger listener rebind before notification actions, and scale invoke-result ack timeout to invoke budget for large clip payloads. (#28260) Thanks @obviyus.</li>
<li>Windows/Plugin install: avoid <code>spawn EINVAL</code> on Windows npm/npx invocations by resolving to <code>node</code> + npm CLI scripts instead of spawning <code>.cmd</code> directly. Landed from contributor PR #31147 by @codertony. Thanks @codertony.</li>
<li>LINE/Voice transcription: classify M4A voice media as <code>audio/mp4</code> (not <code>video/mp4</code>) by checking the MPEG-4 <code>ftyp</code> major brand (<code>M4A </code> / <code>M4B </code>), restoring voice transcription for LINE voice messages. Landed from contributor PR #31151 by @scoootscooob. Thanks @scoootscooob.</li>
<li>Slack/Announce target account routing: enable session-backed announce-target lookup for Slack so multi-account announces resolve the correct <code>accountId</code> instead of defaulting to bot-token context. Landed from contributor PR #31028 by @taw0002. Thanks @taw0002.</li>
<li>Android/Voice screen TTS: stream assistant speech via ElevenLabs WebSocket in Talk Mode, stop cleanly on speaker mute/barge-in, and ignore stale out-of-order stream events. (#29521) Thanks @gregmousseau.</li>
<li>Android/Photos permissions: declare Android 14+ selected-photo access permission (<code>READ_MEDIA_VISUAL_USER_SELECTED</code>) and align Android permission/settings paths with current minSdk behavior for more reliable permission state handling.</li>
<li>Web UI/Cron: include configured agent model defaults/fallbacks in cron model suggestions so scheduled-job model autocomplete reflects configured models. (#29709) Thanks @Sid-Qin.</li>
<li>Cron/Delivery: disable the agent messaging tool when <code>delivery.mode</code> is <code>"none"</code> so cron output is not sent to Telegram or other channels. (#21808) Thanks @lailoo.</li>
<li>CLI/Cron: clarify <code>cron list</code> output by renaming <code>Agent</code> to <code>Agent ID</code> and adding a <code>Model</code> column for isolated agent-turn jobs. (#26259) Thanks @openperf.</li>
<li>Feishu/Reply media attachments: send Feishu reply <code>mediaUrl</code>/<code>mediaUrls</code> payloads as attachments alongside text/streamed replies in the reply dispatcher, including legacy fallback when <code>mediaUrls</code> is empty. (#28959) Thanks @icesword0760.</li>
<li>Slack/User-token resolution: normalize Slack account user-token sourcing through resolved account metadata (<code>SLACK_USER_TOKEN</code> env + config) so monitor reads, Slack actions, directory lookups, onboarding allow-from resolution, and capabilities probing consistently use the effective user token. (#28103) Thanks @Glucksberg.</li>
<li>Feishu/Outbound session routing: stop assuming bare <code>oc_</code> identifiers are always group chats, honor explicit <code>dm:</code>/<code>group:</code> prefixes for <code>oc_</code> chat IDs, and default ambiguous bare <code>oc_</code> targets to direct routing to avoid DM session misclassification. (#10407) Thanks @Bermudarat.</li>
<li>Feishu/Group session routing: add configurable group session scopes (<code>group</code>, <code>group_sender</code>, <code>group_topic</code>, <code>group_topic_sender</code>) with legacy <code>topicSessionMode=enabled</code> compatibility so Feishu group conversations can isolate sessions by sender/topic as configured. (#17798) Thanks @yfge.</li>
<li>Feishu/Reply-in-thread routing: add <code>replyInThread</code> config (<code>disabled|enabled</code>) for group replies, propagate <code>reply_in_thread</code> across text/card/media/streaming sends, and align topic-scoped session routing so newly created reply threads stay on the same session root. (#27325) Thanks @kcinzgg.</li>
<li>Feishu/Probe status caching: cache successful <code>probeFeishu()</code> bot-info results for 10 minutes (bounded cache with per-account keying) to reduce repeated status/onboarding probe API calls, while bypassing cache for failures and exceptions. (#28907) Thanks @Glucksberg.</li>
<li>Feishu/Opus media send type: send <code>.opus</code> attachments with <code>msg_type: "audio"</code> (instead of <code>"media"</code>) so Feishu voice messages deliver correctly while <code>.mp4</code> remains <code>msg_type: "media"</code> and documents remain <code>msg_type: "file"</code>. (#28269) Thanks @Glucksberg.</li>
<li>Feishu/Mobile video media type: treat inbound <code>message_type: "media"</code> as video-equivalent for media key extraction, placeholder inference, and media download resolution so mobile-app video sends ingest correctly. (#25502) Thanks @4ier.</li>
<li>Feishu/Inbound sender fallback: fall back to <code>sender_id.user_id</code> when <code>sender_id.open_id</code> is missing on inbound events, and use ID-type-aware sender lookup so mobile-delivered messages keep stable sender identity/routing. (#26703) Thanks @NewdlDewdl.</li>
<li>Feishu/Reply context metadata: include inbound <code>parent_id</code> and <code>root_id</code> as <code>ReplyToId</code>/<code>RootMessageId</code> in inbound context, and parse interactive-card quote bodies into readable text when fetching replied messages. (#18529) Thanks @qiangu.</li>
<li>Feishu/Post embedded media: extract <code>media</code> tags from inbound rich-text (<code>post</code>) messages and download embedded video/audio files alongside existing embedded-image handling, with regression coverage. (#21786) Thanks @laopuhuluwa.</li>
<li>Feishu/Local media sends: propagate <code>mediaLocalRoots</code> through Feishu outbound media sending into <code>loadWebMedia</code> so local path attachments work with post-CVE local-root enforcement. (#27884) Thanks @joelnishanth.</li>
<li>Feishu/Group wildcard policy fallback: honor <code>channels.feishu.groups["*"]</code> when no explicit group match exists so unmatched groups inherit wildcard reply-policy settings instead of falling back to global defaults. (#29456) Thanks @WaynePika.</li>
<li>Feishu/Inbound media regression coverage: add explicit tests for message resource type mapping (<code>image</code> stays <code>image</code>, non-image maps to <code>file</code>) to prevent reintroducing unsupported Feishu <code>type=audio</code> fetches. (#16311, #8746) Thanks @Yaxuan42.</li>
<li>TTS/Voice bubbles: use opus output and enable <code>audioAsVoice</code> routing for Feishu and WhatsApp (in addition to Telegram) so supported channels receive voice-bubble playback instead of file-style audio attachments. (#27366) Thanks @smthfoxy.</li>
<li>Telegram/Reply media context: include replied media files in inbound context when replying to media, defer reply-media downloads to debounce flush, gate reply-media fetch behind DM authorization, and preserve replied media when non-vision sticker fallback runs (including cached-sticker paths). (#28488) Thanks @obviyus.</li>
<li>Android/Nodes notification wake flow: enable Android <code>system.notify</code> default allowlist, emit <code>notifications.changed</code> events for posted/removed notifications (excluding OpenClaw app-owned notifications), canonicalize notification session keys before enqueue/wake routing, and skip heartbeat wakes when consecutive notification summaries dedupe. (#29440) Thanks @obviyus.</li>
<li>Telegram/Voice fallback reply chunking: apply reply reference, quote text, and inline buttons only to the first fallback text chunk when voice delivery is blocked, preventing over-quoted multi-chunk replies. Landed from contributor PR #31067 by @xdanger. Thanks @xdanger.</li>
<li>Feishu/Multi-account + reply reliability: add <code>channels.feishu.defaultAccount</code> outbound routing support with schema validation, keep quoted-message extraction text-first (post/interactive/file placeholders instead of raw JSON), route Feishu video sends as <code>msg_type: "file"</code>, and avoid websocket event blocking by using non-blocking event handling in monitor dispatch. Landed from contributor PRs #29610, #30432, #30331, and #29501. Thanks @hclsys, @bmendonca3, @patrick-yingxi-pan, and @zwffff.</li>
<li>Cron/Delivery: disable the agent messaging tool when <code>delivery.mode</code> is <code>"none"</code> so cron output is not sent to Telegram or other channels. (#21808) Thanks @lailoo.</li>
<li>Feishu/Inbound rich-text parsing: preserve <code>share_chat</code> payload summaries when available and add explicit parsing for rich-text <code>code</code>/<code>code_block</code>/<code>pre</code> tags so forwarded and code-heavy messages keep useful context in agent input. (#28591) Thanks @kevinWangSheng.</li>
<li>Feishu/Post markdown parsing: parse rich-text <code>post</code> payloads through a shared markdown-aware parser with locale-wrapper support, preserved mention/image metadata extraction, and inline/fenced code fidelity for agent input rendering. (#12755) Thanks @WilsonLiu95.</li>
<li>Telegram/Outbound chunking: route oversize splitting through the shared outbound pipeline (including subagents), retry Telegram sends when escaped HTML exceeds limits, and preserve boundary whitespace when retry re-splitting rendered chunks so plain-text/transcript fidelity is retained. (#29342, #27317; follow-up to #27461) Thanks @obviyus.</li>
<li>Slack/Native commands: register Slack native status as <code>/agentstatus</code> (Slack-reserved <code>/status</code>) so manifest slash command registration stays valid while text <code>/status</code> still works. Landed from contributor PR #29032 by @maloqab. Thanks @maloqab.</li>
<li>Android/Camera clip: remove <code>camera.clip</code> HTTP-upload fallback to base64 so clip transport is deterministic and fail-loud, and reject non-positive <code>maxWidth</code> values so invalid inputs fall back to the safe resize default. (#28229) Thanks @obviyus.</li>
<li>Android/Gateway canvas capability refresh: send <code>node.canvas.capability.refresh</code> with object <code>params</code> (<code>{}</code>) from Android node runtime so gateway object-schema validation accepts refresh retries and A2UI host recovery works after scoped capability expiry. (#28413) Thanks @obviyus.</li>
<li>Gateway/Control UI origins: honor <code>gateway.controlUi.allowedOrigins: ["*"]</code> wildcard entries (including trimmed values) and lock behavior with regression tests. Landed from contributor PR #31058 by @byungsker. Thanks @byungsker.</li>
<li>Web UI/Cron: include configured agent model defaults/fallbacks in cron model suggestions so scheduled-job model autocomplete reflects configured models. (#29709) Thanks @Sid-Qin.</li>
<li>Agents/Sessions list transcript paths: handle missing/non-string/relative <code>sessions.list.path</code> values and per-agent <code>{agentId}</code> templates when deriving <code>transcriptPath</code>, so cross-agent session listings resolve to concrete agent session files instead of workspace-relative paths. (#24775) Thanks @martinfrancois.</li>
<li>Gateway/Control UI CSP: allow required Google Fonts origins in Control UI CSP. (#29279) Thanks @Glucksberg and @vincentkoc.</li>
<li>CLI/Install: add an npm-link fallback to fix CLI startup <code>Permission denied</code> failures (<code>exit 127</code>) on affected installs. (#17151) Thanks @sskyu and @vincentkoc.</li>
<li>Onboarding/Custom providers: improve verification reliability for slower local endpoints (for example Ollama) during setup. (#27380) Thanks @Sid-Qin.</li>
<li>Plugins/NPM spec install: fix npm-spec plugin installs when <code>npm pack</code> output is empty by detecting newly created <code>.tgz</code> archives in the pack directory. (#21039) Thanks @graysurf and @vincentkoc.</li>
<li>Plugins/Install: clear stale install errors when an npm package is not found so follow-up install attempts report current state correctly. (#25073) Thanks @dalefrieswthat.</li>
<li>Security/Feishu webhook ingress: bound unauthenticated webhook rate-limit state with stale-window pruning and a hard key cap to prevent unbounded pre-auth memory growth from rotating source keys. (#26050) Thanks @bmendonca3.</li>
<li>Gateway/macOS supervised restart: actively <code>launchctl kickstart -k</code> during intentional supervised restarts to bypass LaunchAgent <code>ThrottleInterval</code> delays, and fall back to in-process restart when kickstart fails. Landed from contributor PR #29078 by @cathrynlavery. Thanks @cathrynlavery.</li>
<li>Daemon/macOS TLS certs: default LaunchAgent service env <code>NODE_EXTRA_CA_CERTS</code> to <code>/etc/ssl/cert.pem</code> (while preserving explicit overrides) so HTTPS clients no longer fail with local-issuer errors under launchd. (#27915) Thanks @Lukavyi.</li>
<li>Discord/Components wildcard handlers: use distinct internal registration sentinel IDs and parse those sentinels as wildcard keys so select/user/role/channel/mentionable/modal interactions are not dropped by raw customId dedupe paths. Landed from contributor PR #29459 by @Sid-Qin. Thanks @Sid-Qin.</li>
<li>Feishu/Reaction notifications: add <code>channels.feishu.reactionNotifications</code> (<code>off | own | all</code>, default <code>own</code>) so operators can disable reaction ingress or allow all verified reaction events (not only bot-authored message reactions). (#28529) Thanks @cowboy129.</li>
<li>Feishu/Typing backoff: re-throw Feishu typing add/remove rate-limit and quota errors (<code>429</code>, <code>99991400</code>, <code>99991403</code>) and detect SDK non-throwing backoff responses so the typing keepalive circuit breaker can stop retries instead of looping indefinitely. (#28494) Thanks @guoqunabc.</li>
<li>Feishu/Zalo runtime logging: replace direct <code>console.log/error</code> usage in Feishu typing-indicator paths and Zalo monitor paths with runtime-gated logger calls so verbosity controls are respected while preserving typing backoff behavior. (#18841) Thanks @Clawborn.</li>
<li>Feishu/Group sender allowlist fallback: add global <code>channels.feishu.groupSenderAllowFrom</code> sender authorization for group chats, with per-group <code>groups.<id>.allowFrom</code> precedence and regression coverage for allow/block/precedence behavior. (#29174) Thanks @1MoreBuild.</li>
<li>Feishu/Docx append/write ordering: insert converted Docx blocks sequentially (single-block creates) so Feishu append/write preserves markdown block order instead of returning shuffled sections in asynchronous batch inserts. (#26172, #26022) Thanks @echoVic.</li>
<li>Feishu/Docx convert fallback chunking: recursively split oversized markdown chunks (including long no-heading sections) when <code>document.convert</code> hits content limits, while keeping fenced-code-aware split boundaries whenever possible. (#14402) Thanks @lml2468.</li>
<li>Feishu/API quota controls: add <code>typingIndicator</code> and <code>resolveSenderNames</code> config flags (top-level and per-account) so operators can disable typing reactions and sender-name lookup requests while keeping default behavior unchanged. (#10513) Thanks @BigUncle.</li>
<li>Feishu/System preview prompt leakage: stop enqueuing inbound Feishu message previews as system events so user preview text is not injected into later turns as trusted <code>System:</code> context. Landed from contributor PR #31209 by @stakeswky. Thanks @stakeswky.</li>
<li>Feishu/Typing replay suppression: skip typing indicators for stale replayed inbound messages after compaction using message-age checks with second/millisecond timestamp normalization, preventing old-message reaction floods while preserving typing for fresh messages. Landed from contributor PR #30709 by @arkyu2077. Thanks @arkyu2077.</li>
<li>Sessions/Internal routing: preserve established external <code>lastTo</code>/<code>lastChannel</code> routes for internal/non-deliverable turns, with added coverage for no-fallback internal routing behavior. Landed from contributor PR #30941 by @graysurf. Thanks @graysurf.</li>
<li>Control UI/Debug log layout: render Debug Event Log payloads at full width to prevent payload JSON from being squeezed into a narrow side column. Landed from contributor PR #30978 by @stozo04. Thanks @stozo04.</li>
<li>Auto-reply/NO_REPLY: strip <code>NO_REPLY</code> token from mixed-content messages instead of leaking raw control text to end users. Landed from contributor PR #31080 by @scoootscooob. Thanks @scoootscooob.</li>
<li>Install/npm: fix npm global install deprecation warnings. (#28318) Thanks @vincentkoc.</li>
<li>Update/Global npm: fallback to <code>--omit=optional</code> when global <code>npm update</code> fails so optional dependency install failures no longer abort update flows. (#24896) Thanks @xinhuagu and @vincentkoc.</li>
<li>Inbound metadata/Multi-account routing: include <code>account_id</code> in trusted inbound metadata so multi-account channel sessions can reliably disambiguate the receiving account in prompt context. Landed from contributor PR #30984 by @Stxle2. Thanks @Stxle2.</li>
<li>Model directives/Auth profiles: split <code>/model</code> profile suffixes at the first <code>@</code> after the last slash so email-based auth profile IDs (for example OAuth profile IDs) resolve correctly. Landed from contributor PR #30932 by @haosenwang1018. Thanks @haosenwang1018.</li>
<li>Cron/Delivery mode none: send explicit <code>delivery: { mode: "none" }</code> from cron editor for both add and update flows so previous announce delivery is actually cleared. Landed from contributor PR #31145 by @byungsker. Thanks @byungsker.</li>
<li>Cron editor viewport: make the sticky cron edit form independently scrollable with viewport-bounded height so lower fields/actions are reachable on shorter screens. Landed from contributor PR #31133 by @Sid-Qin. Thanks @Sid-Qin.</li>
<li>Agents/Thinking fallback: when providers reject unsupported thinking levels without enumerating alternatives, retry with <code>think=off</code> to avoid hard failure during model/provider fallback chains. Landed from contributor PR #31002 by @yfge. Thanks @yfge.</li>
<li>Ollama/Embedded runner base URL precedence: prioritize configured provider <code>baseUrl</code> over model defaults for embedded Ollama runs so Docker and remote-host setups avoid localhost fetch failures. (#30964) Thanks @stakeswky.</li>
<li>Agents/Failover reason classification: avoid false rate-limit classification from incidental <code>tpm</code> substrings by matching TPM as a standalone token/phrase and keeping auth-context errors on the auth path. Landed from contributor PR #31007 by @HOYALIM. Thanks @HOYALIM.</li>
<li>CLI/Cron: clarify <code>cron list</code> output by renaming <code>Agent</code> to <code>Agent ID</code> and adding a <code>Model</code> column for isolated agent-turn jobs. (#26259) Thanks @openperf.</li>
<li>Gateway/WS: close repeated post-handshake <code>unauthorized role:*</code> request floods per connection and sample duplicate rejection logs, preventing a single misbehaving client from degrading gateway responsiveness. (#20168) Thanks @acy103, @vibecodooor, and @vincentkoc.</li>
<li>Gateway/Auth: improve device-auth v2 migration diagnostics so operators get clearer guidance when legacy clients connect. (#28305) Thanks @vincentkoc.</li>
<li>CLI/Ollama config: allow <code>config set</code> for Ollama <code>apiKey</code> without predeclared provider config. (#29299) Thanks @vincentkoc.</li>
<li>Ollama/Autodiscovery: harden autodiscovery and warning behavior. (#29201) Thanks @marcodelpin and @vincentkoc.</li>
<li>Ollama/Context window: unify context window handling across discovery, merge, and OpenAI-compatible transport paths. (#29205) Thanks @Sid-Qin, @jimmielightner, and @vincentkoc.</li>
<li>Agents/Ollama: demote empty-discovery logging from <code>warn</code> to <code>debug</code> to reduce noisy warnings in normal edge-case discovery flows. (#26379) Thanks @byungsker.</li>
<li>fix(model): preserve reasoning in provider fallback resolution. (#29285) Fixes #25636. Thanks @vincentkoc.</li>
<li>Docker/Image permissions: normalize <code>/app/extensions</code>, <code>/app/.agent</code>, and <code>/app/.agents</code> to directory mode <code>755</code> and file mode <code>644</code> during image build so plugin discovery does not block inherited world-writable paths. (#30191) Fixes #30139. Thanks @edincampara.</li>
<li>OpenAI Responses/Compaction: rewrite and unify the OpenAI Responses store patches to treat empty <code>baseUrl</code> as non-direct, honor <code>compat.supportsStore=false</code>, and auto-inject server-side compaction <code>context_management</code> for compatible direct OpenAI models (with per-model opt-out/threshold overrides). Landed from contributor PRs #16930 (@OiPunk), #22441 (@EdwardWu7), and #25088 (@MoerAI). Thanks @OiPunk, @EdwardWu7, and @MoerAI.</li>
<li>Sandbox/Browser Docker: pass <code>OPENCLAW_BROWSER_NO_SANDBOX=1</code> to sandbox browser containers and bump sandbox browser security hash epoch so existing containers are recreated and pick up the env on upgrade. (#29879) Thanks @Lukavyi.</li>
<li>Usage normalization: clamp negative prompt/input token values to zero (including <code>prompt_tokens</code> alias inputs) so <code>/usage</code> and TUI usage displays cannot show nonsensical negative counts. Landed from contributor PR #31211 by @scoootscooob. Thanks @scoootscooob.</li>
<li>Secrets/Auth profiles: normalize inline SecretRef <code>token</code>/<code>key</code> values to canonical <code>tokenRef</code>/<code>keyRef</code> before persistence, and keep explicit <code>keyRef</code> precedence when inline refs are also present. Landed from contributor PR #31047 by @minupla. Thanks @minupla.</li>
<li>Tools/Edit workspace boundary errors: preserve the real <code>Path escapes workspace root</code> failure path instead of surfacing a misleading access/file-not-found error when editing outside workspace roots. Landed from contributor PR #31015 by @haosenwang1018. Thanks @haosenwang1018.</li>
<li>Browser/Open & navigate: accept <code>url</code> as an alias parameter for <code>open</code> and <code>navigate</code>. (#29260) Thanks @vincentkoc.</li>
<li>Codex/Usage window: label weekly usage window as <code>Week</code> instead of <code>Day</code>. (#26267) Thanks @Sid-Qin.</li>
<li>Signal/Sync message null-handling: treat <code>syncMessage</code> presence (including <code>null</code>) as sync envelope traffic so replayed sentTranscript payloads cannot bypass loop guards after daemon restart. Landed from contributor PR #31138 by @Sid-Qin. Thanks @Sid-Qin.</li>
<li>Infra/fs-safe: sanitize directory-read failures so raw <code>EISDIR</code> text never leaks to messaging surfaces, with regression tests for both root-scoped and direct safe reads. Landed from contributor PR #31205 by @polooooo. Thanks @polooooo.</li>
<li>Sandbox/mkdirp boundary checks: allow directory-safe boundary validation for existing in-boundary subdirectories, preventing false <code>cannot create directories</code> failures in sandbox write mode. (#30610) Thanks @glitch418x.</li>
<li>Security/Compaction audit: remove the post-compaction audit injection message. (#28507) Thanks @fuller-stack-dev and @vincentkoc.</li>
<li>Web tools/RFC2544 fake-IP compatibility: allow RFC2544 benchmark range (<code>198.18.0.0/15</code>) for trusted web-tool fetch endpoints so proxy fake-IP networking modes do not trigger false SSRF blocks. Landed from contributor PR #31176 by @sunkinux. Thanks @sunkinux.</li>
<li>Telegram/Voice fallback reply chunking: apply reply reference, quote text, and inline buttons only to the first fallback text chunk when voice delivery is blocked, preventing over-quoted multi-chunk replies. Landed from contributor PR #31067 by @xdanger. Thanks @xdanger.</li>
<li>Feishu/System preview prompt leakage: stop enqueuing inbound Feishu message previews as system events so user preview text is not injected into later turns as trusted <code>System:</code> context. Landed from contributor PR #31209 by @stakeswky. Thanks @stakeswky.</li>
<li>Feishu/Multi-account + reply reliability: add <code>channels.feishu.defaultAccount</code> outbound routing support with schema validation, keep quoted-message extraction text-first (post/interactive/file placeholders instead of raw JSON), route Feishu video sends as <code>msg_type: "file"</code>, and avoid websocket event blocking by using non-blocking event handling in monitor dispatch. Landed from contributor PRs #29610, #30432, #30331, and #29501. Thanks @hclsys, @bmendonca3, @patrick-yingxi-pan, and @zwffff.</li>
<li>Feishu/Typing replay suppression: skip typing indicators for stale replayed inbound messages after compaction using message-age checks with second/millisecond timestamp normalization, preventing old-message reaction floods while preserving typing for fresh messages. Landed from contributor PR #30709 by @arkyu2077. Thanks @arkyu2077.</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.3.1/OpenClaw-2026.3.1.zip" length="12804155" type="application/octet-stream" sparkle:edSignature="TF1otD4Vk3pG0iViX7mvix5DQEgAsk4JkSFvH7opjf9aawV16f29SUa2wRmiCFU6HEgyNgnGI/078O+A27eXCA=="/>
<!-- pragma: allowlist secret -->
</item>
</channel>
</rss>

View File

@@ -63,8 +63,8 @@ android {
applicationId = "ai.openclaw.app"
minSdk = 31
targetSdk = 36
versionCode = 202603090
versionName = "2026.3.9"
versionCode = 202603081
versionName = "2026.3.8"
ndk {
// Support all major ABIs — native libs are tiny (~47 KB per ABI)
abiFilters += listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")

View File

@@ -17,7 +17,7 @@
<key>CFBundlePackageType</key>
<string>XPC!</string>
<key>CFBundleShortVersionString</key>
<string>2026.3.9</string>
<string>2026.3.8</string>
<key>CFBundleVersion</key>
<string>20260308</string>
<key>NSExtension</key>

View File

@@ -17,7 +17,7 @@
<key>CFBundlePackageType</key>
<string>XPC!</string>
<key>CFBundleShortVersionString</key>
<string>2026.3.9</string>
<string>2026.3.8</string>
<key>CFBundleVersion</key>
<string>20260308</string>
<key>NSExtension</key>

View File

@@ -23,7 +23,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>2026.3.9</string>
<string>2026.3.8</string>
<key>CFBundleURLTypes</key>
<array>
<dict>

View File

@@ -17,7 +17,7 @@
<key>CFBundlePackageType</key>
<string>BNDL</string>
<key>CFBundleShortVersionString</key>
<string>2026.3.9</string>
<string>2026.3.8</string>
<key>CFBundleVersion</key>
<string>20260308</string>
</dict>

View File

@@ -17,7 +17,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>2026.3.9</string>
<string>2026.3.8</string>
<key>CFBundleVersion</key>
<string>20260308</string>
<key>WKCompanionAppBundleIdentifier</key>

View File

@@ -15,7 +15,7 @@
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundleShortVersionString</key>
<string>2026.3.9</string>
<string>2026.3.8</string>
<key>CFBundleVersion</key>
<string>20260308</string>
<key>NSExtension</key>

View File

@@ -107,7 +107,7 @@ targets:
- CFBundleURLName: ai.openclaw.ios
CFBundleURLSchemes:
- openclaw
CFBundleShortVersionString: "2026.3.9"
CFBundleShortVersionString: "2026.3.8"
CFBundleVersion: "20260308"
UILaunchScreen: {}
UIApplicationSceneManifest:
@@ -168,7 +168,7 @@ targets:
path: ShareExtension/Info.plist
properties:
CFBundleDisplayName: OpenClaw Share
CFBundleShortVersionString: "2026.3.9"
CFBundleShortVersionString: "2026.3.8"
CFBundleVersion: "20260308"
NSExtension:
NSExtensionPointIdentifier: com.apple.share-services
@@ -205,7 +205,7 @@ targets:
path: ActivityWidget/Info.plist
properties:
CFBundleDisplayName: OpenClaw Activity
CFBundleShortVersionString: "2026.3.9"
CFBundleShortVersionString: "2026.3.8"
CFBundleVersion: "20260308"
NSSupportsLiveActivities: true
NSExtension:
@@ -231,7 +231,7 @@ targets:
path: WatchApp/Info.plist
properties:
CFBundleDisplayName: OpenClaw
CFBundleShortVersionString: "2026.3.9"
CFBundleShortVersionString: "2026.3.8"
CFBundleVersion: "20260308"
WKCompanionAppBundleIdentifier: "$(OPENCLAW_APP_BUNDLE_ID)"
WKWatchKitApp: true
@@ -256,7 +256,7 @@ targets:
path: WatchExtension/Info.plist
properties:
CFBundleDisplayName: OpenClaw
CFBundleShortVersionString: "2026.3.9"
CFBundleShortVersionString: "2026.3.8"
CFBundleVersion: "20260308"
NSExtension:
NSExtensionAttributes:
@@ -293,7 +293,7 @@ targets:
path: Tests/Info.plist
properties:
CFBundleDisplayName: OpenClawTests
CFBundleShortVersionString: "2026.3.9"
CFBundleShortVersionString: "2026.3.8"
CFBundleVersion: "20260308"
OpenClawLogicTests:
@@ -319,5 +319,5 @@ targets:
path: Tests/Info.plist
properties:
CFBundleDisplayName: OpenClawLogicTests
CFBundleShortVersionString: "2026.3.9"
CFBundleShortVersionString: "2026.3.8"
CFBundleVersion: "20260308"

View File

@@ -15,7 +15,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>2026.3.9</string>
<string>2026.3.8</string>
<key>CFBundleVersion</key>
<string>202603080</string>
<key>CFBundleIconFile</key>

View File

@@ -2504,7 +2504,7 @@ Your gateway is running with auth enabled (`gateway.auth.*`), but the UI is not
Facts (from code):
- The Control UI keeps the token in `sessionStorage` for the current browser tab session and selected gateway URL, so same-tab refreshes keep working without restoring long-lived localStorage token persistence.
- The Control UI keeps the token in memory for the current tab; it no longer persists gateway tokens in browser localStorage.
Fix:

View File

@@ -39,7 +39,7 @@ Notes:
# Default is auto-derived from APP_VERSION when omitted.
SKIP_NOTARIZE=1 \
BUNDLE_ID=ai.openclaw.mac \
APP_VERSION=2026.3.9 \
APP_VERSION=2026.3.8 \
BUILD_CONFIG=release \
SIGN_IDENTITY="Developer ID Application: <Developer Name> (<TEAMID>)" \
scripts/package-mac-dist.sh
@@ -47,10 +47,10 @@ scripts/package-mac-dist.sh
# `package-mac-dist.sh` already creates the zip + DMG.
# If you used `package-mac-app.sh` directly instead, create them manually:
# If you want notarization/stapling in this step, use the NOTARIZE command below.
ditto -c -k --sequesterRsrc --keepParent dist/OpenClaw.app dist/OpenClaw-2026.3.9.zip
ditto -c -k --sequesterRsrc --keepParent dist/OpenClaw.app dist/OpenClaw-2026.3.8.zip
# Optional: build a styled DMG for humans (drag to /Applications)
scripts/create-dmg.sh dist/OpenClaw.app dist/OpenClaw-2026.3.9.dmg
scripts/create-dmg.sh dist/OpenClaw.app dist/OpenClaw-2026.3.8.dmg
# Recommended: build + notarize/staple zip + DMG
# First, create a keychain profile once:
@@ -58,13 +58,13 @@ scripts/create-dmg.sh dist/OpenClaw.app dist/OpenClaw-2026.3.9.dmg
# --apple-id "<apple-id>" --team-id "<team-id>" --password "<app-specific-password>"
NOTARIZE=1 NOTARYTOOL_PROFILE=openclaw-notary \
BUNDLE_ID=ai.openclaw.mac \
APP_VERSION=2026.3.9 \
APP_VERSION=2026.3.8 \
BUILD_CONFIG=release \
SIGN_IDENTITY="Developer ID Application: <Developer Name> (<TEAMID>)" \
scripts/package-mac-dist.sh
# Optional: ship dSYM alongside the release
ditto -c -k --keepParent apps/macos/.build/release/OpenClaw.app.dSYM dist/OpenClaw-2026.3.9.dSYM.zip
ditto -c -k --keepParent apps/macos/.build/release/OpenClaw.app.dSYM dist/OpenClaw-2026.3.8.dSYM.zip
```
## Appcast entry
@@ -72,7 +72,7 @@ ditto -c -k --keepParent apps/macos/.build/release/OpenClaw.app.dSYM dist/OpenCl
Use the release note generator so Sparkle renders formatted HTML notes:
```bash
SPARKLE_PRIVATE_KEY_FILE=/path/to/ed25519-private-key scripts/make_appcast.sh dist/OpenClaw-2026.3.9.zip https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml
SPARKLE_PRIVATE_KEY_FILE=/path/to/ed25519-private-key scripts/make_appcast.sh dist/OpenClaw-2026.3.8.zip https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml
```
Generates HTML release notes from `CHANGELOG.md` (via [`scripts/changelog-to-html.sh`](https://github.com/openclaw/openclaw/blob/main/scripts/changelog-to-html.sh)) and embeds them in the appcast entry.
@@ -80,7 +80,7 @@ Commit the updated `appcast.xml` alongside the release assets (zip + dSYM) when
## Publish & verify
- Upload `OpenClaw-2026.3.9.zip` (and `OpenClaw-2026.3.9.dSYM.zip`) to the GitHub release for tag `v2026.3.9`.
- Upload `OpenClaw-2026.3.8.zip` (and `OpenClaw-2026.3.8.dSYM.zip`) to the GitHub release for tag `v2026.3.8`.
- Ensure the raw appcast URL matches the baked feed: `https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml`.
- Sanity checks:
- `curl -I https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml` returns 200.

View File

@@ -27,7 +27,7 @@ Auth is supplied during the WebSocket handshake via:
- `connect.params.auth.token`
- `connect.params.auth.password`
The dashboard settings panel keeps a token for the current browser tab session and selected gateway URL; passwords are not persisted.
The dashboard settings panel lets you store a token; passwords are not persisted.
The onboarding wizard generates a gateway token by default, so paste it here on first connect.
## Device pairing (first connection)
@@ -237,7 +237,7 @@ http://localhost:5173/?gatewayUrl=wss://<gateway-host>:18789#token=<gateway-toke
Notes:
- `gatewayUrl` is stored in localStorage after load and removed from the URL.
- `token` is imported from the URL fragment, stored in sessionStorage for the current browser tab session and selected gateway URL, and stripped from the URL; it is not stored in localStorage.
- `token` is imported into memory for the current tab and stripped from the URL; it is not stored in localStorage.
- `password` is kept in memory only.
- When `gatewayUrl` is set, the UI does not fall back to config or environment credentials.
Provide `token` (or `password`) explicitly. Missing explicit credentials is an error.

View File

@@ -24,8 +24,8 @@ Authentication is enforced at the WebSocket handshake via `connect.params.auth`
(token or password). See `gateway.auth` in [Gateway configuration](/gateway/configuration).
Security note: the Control UI is an **admin surface** (chat, config, exec approvals).
Do not expose it publicly. The UI keeps dashboard URL tokens in sessionStorage
for the current browser tab session and selected gateway URL, and strips them from the URL after load.
Do not expose it publicly. The UI keeps dashboard URL tokens in memory for the current tab
and strips them from the URL after load.
Prefer localhost, Tailscale Serve, or an SSH tunnel.
## Fast path (recommended)
@@ -37,7 +37,7 @@ Prefer localhost, Tailscale Serve, or an SSH tunnel.
## Token basics (local vs remote)
- **Localhost**: open `http://127.0.0.1:18789/`.
- **Token source**: `gateway.auth.token` (or `OPENCLAW_GATEWAY_TOKEN`); `openclaw dashboard` can pass it via URL fragment for one-time bootstrap, and the Control UI keeps it in sessionStorage for the current browser tab session and selected gateway URL instead of localStorage.
- **Token source**: `gateway.auth.token` (or `OPENCLAW_GATEWAY_TOKEN`); `openclaw dashboard` can pass it via URL fragment for one-time bootstrap, but the Control UI does not persist gateway tokens in localStorage.
- If `gateway.auth.token` is SecretRef-managed, `openclaw dashboard` prints/copies/opens a non-tokenized URL by design. This avoids exposing externally managed tokens in shell logs, clipboard history, or browser-launch arguments.
- If `gateway.auth.token` is configured as a SecretRef and is unresolved in your current shell, `openclaw dashboard` still prints a non-tokenized URL plus actionable auth setup guidance.
- **Not localhost**: use Tailscale Serve (tokenless for Control UI/WebSocket if `gateway.auth.allowTailscale: true`, assumes trusted gateway host; HTTP APIs still need token/password), tailnet bind with a token, or an SSH tunnel. See [Web surfaces](/web).

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/acpx",
"version": "2026.3.9",
"version": "2026.3.8",
"description": "OpenClaw ACP runtime backend via acpx",
"type": "module",
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/bluebubbles",
"version": "2026.3.9",
"version": "2026.3.8",
"description": "OpenClaw BlueBubbles channel plugin",
"type": "module",
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/copilot-proxy",
"version": "2026.3.9",
"version": "2026.3.8",
"private": true,
"description": "OpenClaw Copilot Proxy provider plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/diagnostics-otel",
"version": "2026.3.9",
"version": "2026.3.8",
"description": "OpenClaw diagnostics OpenTelemetry exporter",
"type": "module",
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/diffs",
"version": "2026.3.9",
"version": "2026.3.8",
"private": true,
"description": "OpenClaw diff viewer plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/discord",
"version": "2026.3.9",
"version": "2026.3.8",
"description": "OpenClaw Discord channel plugin",
"type": "module",
"openclaw": {

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/feishu",
"version": "2026.3.9",
"version": "2026.3.8",
"description": "OpenClaw Feishu/Lark channel plugin (community maintained by @m1heng)",
"type": "module",
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/google-gemini-cli-auth",
"version": "2026.3.9",
"version": "2026.3.8",
"private": true,
"description": "OpenClaw Gemini CLI OAuth provider plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/googlechat",
"version": "2026.3.9",
"version": "2026.3.8",
"private": true,
"description": "OpenClaw Google Chat channel plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/imessage",
"version": "2026.3.9",
"version": "2026.3.8",
"private": true,
"description": "OpenClaw iMessage channel plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/irc",
"version": "2026.3.9",
"version": "2026.3.8",
"description": "OpenClaw IRC channel plugin",
"type": "module",
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/line",
"version": "2026.3.9",
"version": "2026.3.8",
"private": true,
"description": "OpenClaw LINE channel plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/llm-task",
"version": "2026.3.9",
"version": "2026.3.8",
"private": true,
"description": "OpenClaw JSON-only LLM task plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/lobster",
"version": "2026.3.9",
"version": "2026.3.8",
"description": "Lobster workflow tool plugin (typed pipelines + resumable approvals)",
"type": "module",
"dependencies": {

View File

@@ -1,17 +1,5 @@
# Changelog
## 2026.3.9
### Changes
- Version alignment with core OpenClaw release numbers.
## 2026.3.8-beta.1
### Changes
- Version alignment with core OpenClaw release numbers.
## 2026.3.8
### Changes

View File

@@ -1,10 +1,10 @@
{
"name": "@openclaw/matrix",
"version": "2026.3.9",
"version": "2026.3.8",
"description": "OpenClaw Matrix channel plugin",
"type": "module",
"dependencies": {
"@mariozechner/pi-agent-core": "0.57.1",
"@mariozechner/pi-agent-core": "0.55.3",
"@matrix-org/matrix-sdk-crypto-nodejs": "^0.4.0",
"@vector-im/matrix-bot-sdk": "0.8.0-element.3",
"markdown-it": "14.1.1",

View File

@@ -1,400 +1,65 @@
import type { MatrixClient } from "@vector-im/matrix-bot-sdk";
import { describe, expect, it, vi } from "vitest";
import { createDirectRoomTracker } from "./direct.js";
// ---------------------------------------------------------------------------
// Helpers -- minimal MatrixClient stub
// ---------------------------------------------------------------------------
type StateEvent = Record<string, unknown>;
type DmMap = Record<string, boolean>;
function createMockClient(opts: {
dmRooms?: DmMap;
membersByRoom?: Record<string, string[]>;
stateEvents?: Record<string, StateEvent>;
selfUserId?: string;
function createMockClient(params: {
isDm?: boolean;
senderDirect?: boolean;
selfDirect?: boolean;
members?: string[];
}) {
const {
dmRooms = {},
membersByRoom = {},
stateEvents = {},
selfUserId = "@bot:example.org",
} = opts;
const members = params.members ?? ["@alice:example.org", "@bot:example.org"];
return {
dms: {
isDm: (roomId: string) => dmRooms[roomId] ?? false,
update: vi.fn().mockResolvedValue(undefined),
isDm: vi.fn().mockReturnValue(params.isDm === true),
},
getUserId: vi.fn().mockResolvedValue(selfUserId),
getJoinedRoomMembers: vi.fn().mockImplementation(async (roomId: string) => {
return membersByRoom[roomId] ?? [];
}),
getUserId: vi.fn().mockResolvedValue("@bot:example.org"),
getJoinedRoomMembers: vi.fn().mockResolvedValue(members),
getRoomStateEvent: vi
.fn()
.mockImplementation(async (roomId: string, eventType: string, stateKey: string) => {
const key = `${roomId}|${eventType}|${stateKey}`;
const ev = stateEvents[key];
if (ev === undefined) {
// Simulate real homeserver M_NOT_FOUND response (matches MatrixError shape)
const err = new Error(`State event not found: ${key}`) as Error & {
errcode?: string;
statusCode?: number;
};
err.errcode = "M_NOT_FOUND";
err.statusCode = 404;
throw err;
.mockImplementation(async (_roomId: string, _event: string, stateKey: string) => {
if (stateKey === "@alice:example.org") {
return { is_direct: params.senderDirect === true };
}
return ev;
if (stateKey === "@bot:example.org") {
return { is_direct: params.selfDirect === true };
}
return {};
}),
};
} as unknown as MatrixClient;
}
// ---------------------------------------------------------------------------
// Tests -- isDirectMessage
// ---------------------------------------------------------------------------
describe("createDirectRoomTracker", () => {
describe("m.direct detection (SDK DM cache)", () => {
it("returns true when SDK DM cache marks room as DM", async () => {
const client = createMockClient({
dmRooms: { "!dm:example.org": true },
});
const tracker = createDirectRoomTracker(client as never);
const result = await tracker.isDirectMessage({
roomId: "!dm:example.org",
senderId: "@alice:example.org",
});
expect(result).toBe(true);
});
it("returns false for rooms not in SDK DM cache (with >2 members)", async () => {
const client = createMockClient({
dmRooms: {},
membersByRoom: {
"!group:example.org": ["@alice:example.org", "@bob:example.org", "@carol:example.org"],
},
});
const tracker = createDirectRoomTracker(client as never);
const result = await tracker.isDirectMessage({
roomId: "!group:example.org",
senderId: "@alice:example.org",
});
expect(result).toBe(false);
});
});
describe("is_direct state flag detection", () => {
it("returns true when sender's membership has is_direct=true", async () => {
const client = createMockClient({
dmRooms: {},
membersByRoom: { "!room:example.org": ["@alice:example.org", "@bot:example.org"] },
stateEvents: {
"!room:example.org|m.room.member|@alice:example.org": { is_direct: true },
"!room:example.org|m.room.member|@bot:example.org": { is_direct: false },
},
});
const tracker = createDirectRoomTracker(client as never);
const result = await tracker.isDirectMessage({
it("treats m.direct rooms as DMs", async () => {
const tracker = createDirectRoomTracker(createMockClient({ isDm: true }));
await expect(
tracker.isDirectMessage({
roomId: "!room:example.org",
senderId: "@alice:example.org",
});
}),
).resolves.toBe(true);
});
expect(result).toBe(true);
});
it("returns true when bot's own membership has is_direct=true", async () => {
const client = createMockClient({
dmRooms: {},
membersByRoom: { "!room:example.org": ["@alice:example.org", "@bot:example.org"] },
stateEvents: {
"!room:example.org|m.room.member|@alice:example.org": { is_direct: false },
"!room:example.org|m.room.member|@bot:example.org": { is_direct: true },
},
});
const tracker = createDirectRoomTracker(client as never);
const result = await tracker.isDirectMessage({
it("does not classify 2-member rooms as DMs without direct flags", async () => {
const client = createMockClient({ isDm: false });
const tracker = createDirectRoomTracker(client);
await expect(
tracker.isDirectMessage({
roomId: "!room:example.org",
senderId: "@alice:example.org",
selfUserId: "@bot:example.org",
});
expect(result).toBe(true);
});
}),
).resolves.toBe(false);
expect(client.getJoinedRoomMembers).not.toHaveBeenCalled();
});
describe("conservative fallback (memberCount + room name)", () => {
it("returns true for 2-member room WITHOUT a room name (broken flags)", async () => {
const client = createMockClient({
dmRooms: {},
membersByRoom: {
"!broken-dm:example.org": ["@alice:example.org", "@bot:example.org"],
},
stateEvents: {
// is_direct not set on either member (e.g. Continuwuity bug)
"!broken-dm:example.org|m.room.member|@alice:example.org": {},
"!broken-dm:example.org|m.room.member|@bot:example.org": {},
// No m.room.name -> getRoomStateEvent will throw (event not found)
},
});
const tracker = createDirectRoomTracker(client as never);
const result = await tracker.isDirectMessage({
roomId: "!broken-dm:example.org",
senderId: "@alice:example.org",
});
expect(result).toBe(true);
});
it("returns true for 2-member room with empty room name", async () => {
const client = createMockClient({
dmRooms: {},
membersByRoom: {
"!broken-dm:example.org": ["@alice:example.org", "@bot:example.org"],
},
stateEvents: {
"!broken-dm:example.org|m.room.member|@alice:example.org": {},
"!broken-dm:example.org|m.room.member|@bot:example.org": {},
"!broken-dm:example.org|m.room.name|": { name: "" },
},
});
const tracker = createDirectRoomTracker(client as never);
const result = await tracker.isDirectMessage({
roomId: "!broken-dm:example.org",
senderId: "@alice:example.org",
});
expect(result).toBe(true);
});
it("returns false for 2-member room WITH a room name (named group)", async () => {
const client = createMockClient({
dmRooms: {},
membersByRoom: {
"!named-group:example.org": ["@alice:example.org", "@bob:example.org"],
},
stateEvents: {
"!named-group:example.org|m.room.member|@alice:example.org": {},
"!named-group:example.org|m.room.member|@bob:example.org": {},
"!named-group:example.org|m.room.name|": { name: "Project Alpha" },
},
});
const tracker = createDirectRoomTracker(client as never);
const result = await tracker.isDirectMessage({
roomId: "!named-group:example.org",
senderId: "@alice:example.org",
});
expect(result).toBe(false);
});
it("returns false for 3+ member room without any DM signals", async () => {
const client = createMockClient({
dmRooms: {},
membersByRoom: {
"!group:example.org": ["@alice:example.org", "@bob:example.org", "@carol:example.org"],
},
stateEvents: {
"!group:example.org|m.room.member|@alice:example.org": {},
"!group:example.org|m.room.member|@bob:example.org": {},
"!group:example.org|m.room.member|@carol:example.org": {},
},
});
const tracker = createDirectRoomTracker(client as never);
const result = await tracker.isDirectMessage({
roomId: "!group:example.org",
senderId: "@alice:example.org",
});
expect(result).toBe(false);
});
it("returns false for 1-member room (self-chat)", async () => {
const client = createMockClient({
dmRooms: {},
membersByRoom: {
"!solo:example.org": ["@bot:example.org"],
},
stateEvents: {
"!solo:example.org|m.room.member|@bot:example.org": {},
},
});
const tracker = createDirectRoomTracker(client as never);
const result = await tracker.isDirectMessage({
roomId: "!solo:example.org",
senderId: "@bot:example.org",
});
expect(result).toBe(false);
});
});
describe("detection priority", () => {
it("m.direct takes priority -- skips state and fallback checks", async () => {
const client = createMockClient({
dmRooms: { "!dm:example.org": true },
membersByRoom: {
"!dm:example.org": ["@alice:example.org", "@bob:example.org", "@carol:example.org"],
},
stateEvents: {
"!dm:example.org|m.room.name|": { name: "Named Room" },
},
});
const tracker = createDirectRoomTracker(client as never);
const result = await tracker.isDirectMessage({
roomId: "!dm:example.org",
senderId: "@alice:example.org",
});
expect(result).toBe(true);
// Should not have checked member state or room name
expect(client.getRoomStateEvent).not.toHaveBeenCalled();
expect(client.getJoinedRoomMembers).not.toHaveBeenCalled();
});
it("is_direct takes priority over fallback -- skips member count", async () => {
const client = createMockClient({
dmRooms: {},
stateEvents: {
"!room:example.org|m.room.member|@alice:example.org": { is_direct: true },
},
});
const tracker = createDirectRoomTracker(client as never);
const result = await tracker.isDirectMessage({
it("uses is_direct member flags when present", async () => {
const tracker = createDirectRoomTracker(createMockClient({ senderDirect: true }));
await expect(
tracker.isDirectMessage({
roomId: "!room:example.org",
senderId: "@alice:example.org",
});
expect(result).toBe(true);
// Should not have checked member count
expect(client.getJoinedRoomMembers).not.toHaveBeenCalled();
});
});
describe("edge cases", () => {
it("handles member count API failure gracefully", async () => {
const client = createMockClient({
dmRooms: {},
stateEvents: {
"!failing:example.org|m.room.member|@alice:example.org": {},
"!failing:example.org|m.room.member|@bot:example.org": {},
},
});
client.getJoinedRoomMembers.mockRejectedValue(new Error("API unavailable"));
const tracker = createDirectRoomTracker(client as never);
const result = await tracker.isDirectMessage({
roomId: "!failing:example.org",
senderId: "@alice:example.org",
});
// Cannot determine member count -> conservative: classify as group
expect(result).toBe(false);
});
it("treats M_NOT_FOUND for room name as no name (DM)", async () => {
const client = createMockClient({
dmRooms: {},
membersByRoom: {
"!no-name:example.org": ["@alice:example.org", "@bot:example.org"],
},
stateEvents: {
"!no-name:example.org|m.room.member|@alice:example.org": {},
"!no-name:example.org|m.room.member|@bot:example.org": {},
// m.room.name not in stateEvents -> mock throws generic Error
},
});
// Override to throw M_NOT_FOUND like a real homeserver
const originalImpl = client.getRoomStateEvent.getMockImplementation()!;
client.getRoomStateEvent.mockImplementation(
async (roomId: string, eventType: string, stateKey: string) => {
if (eventType === "m.room.name") {
const err = new Error("not found") as Error & {
errcode?: string;
statusCode?: number;
};
err.errcode = "M_NOT_FOUND";
err.statusCode = 404;
throw err;
}
return originalImpl(roomId, eventType, stateKey);
},
);
const tracker = createDirectRoomTracker(client as never);
const result = await tracker.isDirectMessage({
roomId: "!no-name:example.org",
senderId: "@alice:example.org",
});
expect(result).toBe(true);
});
it("treats non-404 room name errors as unknown (falls through to group)", async () => {
const client = createMockClient({
dmRooms: {},
membersByRoom: {
"!error-room:example.org": ["@alice:example.org", "@bot:example.org"],
},
stateEvents: {
"!error-room:example.org|m.room.member|@alice:example.org": {},
"!error-room:example.org|m.room.member|@bot:example.org": {},
},
});
// Simulate a network/auth error (not M_NOT_FOUND)
const originalImpl = client.getRoomStateEvent.getMockImplementation()!;
client.getRoomStateEvent.mockImplementation(
async (roomId: string, eventType: string, stateKey: string) => {
if (eventType === "m.room.name") {
throw new Error("Connection refused");
}
return originalImpl(roomId, eventType, stateKey);
},
);
const tracker = createDirectRoomTracker(client as never);
const result = await tracker.isDirectMessage({
roomId: "!error-room:example.org",
senderId: "@alice:example.org",
});
// Network error -> don't assume DM, classify as group
expect(result).toBe(false);
});
it("whitespace-only room name is treated as no name", async () => {
const client = createMockClient({
dmRooms: {},
membersByRoom: {
"!ws-name:example.org": ["@alice:example.org", "@bot:example.org"],
},
stateEvents: {
"!ws-name:example.org|m.room.member|@alice:example.org": {},
"!ws-name:example.org|m.room.member|@bot:example.org": {},
"!ws-name:example.org|m.room.name|": { name: " " },
},
});
const tracker = createDirectRoomTracker(client as never);
const result = await tracker.isDirectMessage({
roomId: "!ws-name:example.org",
senderId: "@alice:example.org",
});
expect(result).toBe(true);
});
}),
).resolves.toBe(true);
});
});

View File

@@ -13,22 +13,14 @@ type DirectRoomTrackerOptions = {
const DM_CACHE_TTL_MS = 30_000;
/**
* Check if an error is a Matrix M_NOT_FOUND response (missing state event).
* The bot-sdk throws MatrixError with errcode/statusCode on the error object.
*/
function isMatrixNotFoundError(err: unknown): boolean {
if (typeof err !== "object" || err === null) return false;
const e = err as { errcode?: string; statusCode?: number };
return e.errcode === "M_NOT_FOUND" || e.statusCode === 404;
}
export function createDirectRoomTracker(client: MatrixClient, opts: DirectRoomTrackerOptions = {}) {
const log = opts.log ?? (() => {});
const includeMemberCountInLogs = opts.includeMemberCountInLogs === true;
let lastDmUpdateMs = 0;
let cachedSelfUserId: string | null = null;
const memberCountCache = new Map<string, { count: number; ts: number }>();
const memberCountCache = includeMemberCountInLogs
? new Map<string, { count: number; ts: number }>()
: undefined;
const ensureSelfUserId = async (): Promise<string | null> => {
if (cachedSelfUserId) {
@@ -56,6 +48,9 @@ export function createDirectRoomTracker(client: MatrixClient, opts: DirectRoomTr
};
const resolveMemberCount = async (roomId: string): Promise<number | null> => {
if (!memberCountCache) {
return null;
}
const cached = memberCountCache.get(roomId);
const now = Date.now();
if (cached && now - cached.ts < DM_CACHE_TTL_MS) {
@@ -96,6 +91,7 @@ export function createDirectRoomTracker(client: MatrixClient, opts: DirectRoomTr
return true;
}
// Check m.room.member state for is_direct flag
const selfUserId = params.selfUserId ?? (await ensureSelfUserId());
const directViaState =
(await hasDirectFlag(roomId, senderId)) || (await hasDirectFlag(roomId, selfUserId ?? ""));
@@ -104,47 +100,16 @@ export function createDirectRoomTracker(client: MatrixClient, opts: DirectRoomTr
return true;
}
// Conservative fallback: 2-member rooms without an explicit room name are likely
// DMs with broken m.direct / is_direct flags. This has been observed on Continuwuity
// where m.direct pointed to the wrong room and is_direct was never set on the invite.
// Unlike the removed heuristic, this requires two signals (member count + no name)
// to avoid false positives on named 2-person group rooms.
//
// Performance: member count is cached (resolveMemberCount). The room name state
// check is not cached but only runs for the subset of 2-member rooms that reach
// this fallback path (no m.direct, no is_direct). In typical deployments this is
// a small minority of rooms.
//
// Note: there is a narrow race where a room name is being set concurrently with
// this check. The consequence is a one-time misclassification that self-corrects
// on the next message (once the state event is synced). This is acceptable given
// the alternative of an additional API call on every message.
const memberCount = await resolveMemberCount(roomId);
if (memberCount === 2) {
try {
const nameState = await client.getRoomStateEvent(roomId, "m.room.name", "");
if (!nameState?.name?.trim()) {
log(`matrix: dm detected via fallback (2 members, no room name) room=${roomId}`);
return true;
}
} catch (err: unknown) {
// Missing state events (M_NOT_FOUND) are expected for unnamed rooms and
// strongly indicate a DM. Any other error (network, auth) is ambiguous,
// so we fall through to classify as group rather than guess.
if (isMatrixNotFoundError(err)) {
log(`matrix: dm detected via fallback (2 members, no room name) room=${roomId}`);
return true;
}
log(
`matrix: dm fallback skipped (room name check failed: ${String(err)}) room=${roomId}`,
);
}
}
// Member count alone is NOT a reliable DM indicator.
// Explicitly configured group rooms with 2 members (e.g. bot + one user)
// were being misclassified as DMs, causing messages to be routed through
// DM policy instead of group policy and silently dropped.
// See: https://github.com/openclaw/openclaw/issues/20145
if (!includeMemberCountInLogs) {
log(`matrix: dm check room=${roomId} result=group`);
return false;
}
const memberCount = await resolveMemberCount(roomId);
log(`matrix: dm check room=${roomId} result=group members=${memberCount ?? "unknown"}`);
return false;
},

View File

@@ -1,11 +1,7 @@
import type { MatrixClient } from "@vector-im/matrix-bot-sdk";
import type { PluginRuntime, RuntimeEnv, RuntimeLogger } from "openclaw/plugin-sdk/matrix";
import { describe, expect, it, vi } from "vitest";
import {
createMatrixRoomMessageHandler,
resolveMatrixBaseRouteSession,
shouldOverrideMatrixDmToGroup,
} from "./handler.js";
import { createMatrixRoomMessageHandler } from "./handler.js";
import { EventType, type MatrixRawEvent } from "./types.js";
describe("createMatrixRoomMessageHandler BodyForAgent sender label", () => {
@@ -22,15 +18,8 @@ describe("createMatrixRoomMessageHandler BodyForAgent sender label", () => {
channel: {
pairing: {
readAllowFromStore: vi.fn().mockResolvedValue([]),
upsertPairingRequest: vi.fn().mockResolvedValue(undefined),
},
routing: {
buildAgentSessionKey: vi
.fn()
.mockImplementation(
(params: { agentId: string; channel: string; peer?: { kind: string; id: string } }) =>
`agent:${params.agentId}:${params.channel}:${params.peer?.kind ?? "direct"}:${params.peer?.id ?? "unknown"}`,
),
resolveAgentRoute: vi.fn().mockReturnValue({
agentId: "main",
accountId: undefined,
@@ -150,47 +139,4 @@ describe("createMatrixRoomMessageHandler BodyForAgent sender label", () => {
}),
);
});
it("uses room-scoped session keys for DM rooms matched via parentPeer binding", () => {
const buildAgentSessionKey = vi
.fn()
.mockReturnValue("agent:main:matrix:channel:!dmroom:example.org");
const resolved = resolveMatrixBaseRouteSession({
buildAgentSessionKey,
baseRoute: {
agentId: "main",
sessionKey: "agent:main:main",
mainSessionKey: "agent:main:main",
matchedBy: "binding.peer.parent",
},
isDirectMessage: true,
roomId: "!dmroom:example.org",
accountId: undefined,
});
expect(buildAgentSessionKey).toHaveBeenCalledWith({
agentId: "main",
channel: "matrix",
accountId: undefined,
peer: { kind: "channel", id: "!dmroom:example.org" },
});
expect(resolved).toEqual({
sessionKey: "agent:main:matrix:channel:!dmroom:example.org",
lastRoutePolicy: "session",
});
});
it("does not override DMs to groups for explicit allow:false room config", () => {
expect(
shouldOverrideMatrixDmToGroup({
isDirectMessage: true,
roomConfigInfo: {
config: { allow: false },
allowed: false,
matchSource: "direct",
},
}),
).toBe(false);
});
});

View File

@@ -77,56 +77,6 @@ export type MatrixMonitorHandlerParams = {
accountId?: string | null;
};
export function resolveMatrixBaseRouteSession(params: {
buildAgentSessionKey: (params: {
agentId: string;
channel: string;
accountId?: string | null;
peer?: { kind: "direct" | "channel"; id: string } | null;
}) => string;
baseRoute: {
agentId: string;
sessionKey: string;
mainSessionKey: string;
matchedBy?: string;
};
isDirectMessage: boolean;
roomId: string;
accountId?: string | null;
}): { sessionKey: string; lastRoutePolicy: "main" | "session" } {
const sessionKey =
params.isDirectMessage && params.baseRoute.matchedBy === "binding.peer.parent"
? params.buildAgentSessionKey({
agentId: params.baseRoute.agentId,
channel: "matrix",
accountId: params.accountId,
peer: { kind: "channel", id: params.roomId },
})
: params.baseRoute.sessionKey;
return {
sessionKey,
lastRoutePolicy: sessionKey === params.baseRoute.mainSessionKey ? "main" : "session",
};
}
export function shouldOverrideMatrixDmToGroup(params: {
isDirectMessage: boolean;
roomConfigInfo?:
| {
config?: MatrixRoomConfig;
allowed: boolean;
matchSource?: string;
}
| undefined;
}): boolean {
return (
params.isDirectMessage === true &&
params.roomConfigInfo?.config !== undefined &&
params.roomConfigInfo.allowed === true &&
params.roomConfigInfo.matchSource === "direct"
);
}
export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParams) {
const {
client,
@@ -238,37 +188,22 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
}
}
let isDirectMessage = await directTracker.isDirectMessage({
const isDirectMessage = await directTracker.isDirectMessage({
roomId,
senderId,
selfUserId,
});
// Resolve room config early so explicitly configured rooms can override DM classification.
// This ensures rooms in the groups config are always treated as groups regardless of
// member count or protocol-level DM flags. Only explicit matches (not wildcards) trigger
// the override to avoid breaking DM routing when a wildcard entry exists. (See #9106)
const roomConfigInfo = resolveMatrixRoomConfig({
rooms: roomsConfig,
roomId,
aliases: roomAliases,
name: roomName,
});
if (shouldOverrideMatrixDmToGroup({ isDirectMessage, roomConfigInfo })) {
logVerboseMessage(
`matrix: overriding DM to group for configured room=${roomId} (${roomConfigInfo.matchKey})`,
);
isDirectMessage = false;
}
const isRoom = !isDirectMessage;
if (isRoom && groupPolicy === "disabled") {
return;
}
// Only expose room config for confirmed group rooms. DMs should never inherit
// group settings (skills, systemPrompt, autoReply) even when a wildcard entry exists.
const roomConfig = isRoom ? roomConfigInfo?.config : undefined;
const roomConfigInfo = isRoom
? resolveMatrixRoomConfig({
rooms: roomsConfig,
roomId,
aliases: roomAliases,
name: roomName,
})
: undefined;
const roomConfig = roomConfigInfo?.config;
const roomMatchMeta = roomConfigInfo
? `matchKey=${roomConfigInfo.matchKey ?? "none"} matchSource=${
roomConfigInfo.matchSource ?? "none"
@@ -500,24 +435,13 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
kind: isDirectMessage ? "direct" : "channel",
id: isDirectMessage ? senderId : roomId,
},
// For DMs, pass roomId as parentPeer so the conversation is bindable by room ID
// while preserving DM trust semantics (secure 1:1, no group restrictions).
parentPeer: isDirectMessage ? { kind: "channel", id: roomId } : undefined,
});
const baseRouteSession = resolveMatrixBaseRouteSession({
buildAgentSessionKey: core.channel.routing.buildAgentSessionKey,
baseRoute,
isDirectMessage,
roomId,
accountId,
});
const route = {
...baseRoute,
lastRoutePolicy: baseRouteSession.lastRoutePolicy,
sessionKey: threadRootId
? `${baseRouteSession.sessionKey}:thread:${threadRootId}`
: baseRouteSession.sessionKey,
? `${baseRoute.sessionKey}:thread:${threadRootId}`
: baseRoute.sessionKey,
};
let threadStarterBody: string | undefined;

View File

@@ -36,89 +36,4 @@ describe("resolveMatrixRoomConfig", () => {
expect(byName.allowed).toBe(false);
expect(byName.config).toBeUndefined();
});
describe("matchSource classification", () => {
it('returns matchSource="direct" for exact room ID match', () => {
const result = resolveMatrixRoomConfig({
rooms: { "!room:example.org": { allow: true } },
roomId: "!room:example.org",
aliases: [],
});
expect(result.matchSource).toBe("direct");
expect(result.config).toBeDefined();
});
it('returns matchSource="direct" for alias match', () => {
const result = resolveMatrixRoomConfig({
rooms: { "#alias:example.org": { allow: true } },
roomId: "!room:example.org",
aliases: ["#alias:example.org"],
});
expect(result.matchSource).toBe("direct");
expect(result.config).toBeDefined();
});
it('returns matchSource="wildcard" for wildcard match', () => {
const result = resolveMatrixRoomConfig({
rooms: { "*": { allow: true } },
roomId: "!any:example.org",
aliases: [],
});
expect(result.matchSource).toBe("wildcard");
expect(result.config).toBeDefined();
});
it("returns undefined matchSource when no match", () => {
const result = resolveMatrixRoomConfig({
rooms: { "!other:example.org": { allow: true } },
roomId: "!room:example.org",
aliases: [],
});
expect(result.matchSource).toBeUndefined();
expect(result.config).toBeUndefined();
});
it("direct match takes priority over wildcard", () => {
const result = resolveMatrixRoomConfig({
rooms: {
"!room:example.org": { allow: true, systemPrompt: "room-specific" },
"*": { allow: true, systemPrompt: "generic" },
},
roomId: "!room:example.org",
aliases: [],
});
expect(result.matchSource).toBe("direct");
expect(result.config?.systemPrompt).toBe("room-specific");
});
});
describe("DM override safety (matchSource distinction)", () => {
// These tests verify the matchSource property that handler.ts uses
// to decide whether a configured room should override DM classification.
// Only "direct" matches should trigger the override -- never "wildcard".
it("wildcard config should NOT be usable to override DM classification", () => {
const result = resolveMatrixRoomConfig({
rooms: { "*": { allow: true, skills: ["general"] } },
roomId: "!dm-room:example.org",
aliases: [],
});
// handler.ts checks: matchSource === "direct" -> this is "wildcard", so no override
expect(result.matchSource).not.toBe("direct");
expect(result.matchSource).toBe("wildcard");
});
it("explicitly configured room should be usable to override DM classification", () => {
const result = resolveMatrixRoomConfig({
rooms: {
"!configured-room:example.org": { allow: true },
"*": { allow: true },
},
roomId: "!configured-room:example.org",
aliases: [],
});
// handler.ts checks: matchSource === "direct" -> this IS "direct", so override is safe
expect(result.matchSource).toBe("direct");
});
});
});

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/mattermost",
"version": "2026.3.9",
"version": "2026.3.8",
"description": "OpenClaw Mattermost channel plugin",
"type": "module",
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/memory-core",
"version": "2026.3.9",
"version": "2026.3.8",
"private": true,
"description": "OpenClaw core memory search plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/memory-lancedb",
"version": "2026.3.9",
"version": "2026.3.8",
"private": true,
"description": "OpenClaw LanceDB-backed long-term memory plugin with auto-recall/capture",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/minimax-portal-auth",
"version": "2026.3.9",
"version": "2026.3.8",
"private": true,
"description": "OpenClaw MiniMax Portal OAuth provider plugin",
"type": "module",

View File

@@ -1,17 +1,5 @@
# Changelog
## 2026.3.9
### Changes
- Version alignment with core OpenClaw release numbers.
## 2026.3.8-beta.1
### Changes
- Version alignment with core OpenClaw release numbers.
## 2026.3.8
### Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/msteams",
"version": "2026.3.9",
"version": "2026.3.8",
"description": "OpenClaw Microsoft Teams channel plugin",
"type": "module",
"dependencies": {

View File

@@ -5,7 +5,7 @@ import { setMSTeamsRuntime } from "../runtime.js";
import { createMSTeamsMessageHandler } from "./message-handler.js";
describe("msteams monitor handler authz", () => {
function createDeps(cfg: OpenClawConfig) {
it("does not treat DM pairing-store entries as group allowlist entries", async () => {
const readAllowFromStore = vi.fn(async () => ["attacker-aad"]);
setMSTeamsRuntime({
logging: { shouldLogVerbose: () => false },
@@ -35,7 +35,16 @@ describe("msteams monitor handler authz", () => {
};
const deps: MSTeamsMessageHandlerDeps = {
cfg,
cfg: {
channels: {
msteams: {
dmPolicy: "pairing",
allowFrom: [],
groupPolicy: "allowlist",
groupAllowFrom: [],
},
},
} as OpenClawConfig,
runtime: { error: vi.fn() } as unknown as RuntimeEnv,
appId: "test-app",
adapter: {} as MSTeamsMessageHandlerDeps["adapter"],
@@ -56,21 +65,6 @@ describe("msteams monitor handler authz", () => {
} as unknown as MSTeamsMessageHandlerDeps["log"],
};
return { conversationStore, deps, readAllowFromStore };
}
it("does not treat DM pairing-store entries as group allowlist entries", async () => {
const { conversationStore, deps, readAllowFromStore } = createDeps({
channels: {
msteams: {
dmPolicy: "pairing",
allowFrom: [],
groupPolicy: "allowlist",
groupAllowFrom: [],
},
},
} as OpenClawConfig);
const handler = createMSTeamsMessageHandler(deps);
await handler({
activity: {
@@ -102,54 +96,4 @@ describe("msteams monitor handler authz", () => {
});
expect(conversationStore.upsert).not.toHaveBeenCalled();
});
it("does not widen sender auth when only a teams route allowlist is configured", async () => {
const { conversationStore, deps } = createDeps({
channels: {
msteams: {
dmPolicy: "pairing",
allowFrom: [],
groupPolicy: "allowlist",
groupAllowFrom: [],
teams: {
team123: {
channels: {
"19:group@thread.tacv2": { requireMention: false },
},
},
},
},
},
} as OpenClawConfig);
const handler = createMSTeamsMessageHandler(deps);
await handler({
activity: {
id: "msg-1",
type: "message",
text: "hello",
from: {
id: "attacker-id",
aadObjectId: "attacker-aad",
name: "Attacker",
},
recipient: {
id: "bot-id",
name: "Bot",
},
conversation: {
id: "19:group@thread.tacv2",
conversationType: "groupChat",
},
channelData: {
team: { id: "team123", name: "Team 123" },
channel: { name: "General" },
},
attachments: [],
},
sendActivity: vi.fn(async () => undefined),
} as unknown as Parameters<typeof handler>[0]);
expect(conversationStore.upsert).not.toHaveBeenCalled();
});
});

View File

@@ -242,7 +242,10 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
}
const senderGroupAccess = evaluateSenderGroupAccessForPolicy({
groupPolicy,
groupAllowFrom: effectiveGroupAllowFrom,
groupAllowFrom:
effectiveGroupAllowFrom.length > 0 || !channelGate.allowlistConfigured
? effectiveGroupAllowFrom
: ["*"],
senderId,
isSenderAllowed: (_senderId, allowFrom) =>
resolveMSTeamsAllowlistMatch({

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/nextcloud-talk",
"version": "2026.3.9",
"version": "2026.3.8",
"description": "OpenClaw Nextcloud Talk channel plugin",
"type": "module",
"dependencies": {

View File

@@ -1,17 +1,5 @@
# Changelog
## 2026.3.9
### Changes
- Version alignment with core OpenClaw release numbers.
## 2026.3.8-beta.1
### Changes
- Version alignment with core OpenClaw release numbers.
## 2026.3.8
### Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/nostr",
"version": "2026.3.9",
"version": "2026.3.8",
"description": "OpenClaw Nostr channel plugin for NIP-04 encrypted DMs",
"type": "module",
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/open-prose",
"version": "2026.3.9",
"version": "2026.3.8",
"private": true,
"description": "OpenProse VM skill pack plugin (slash command + telemetry).",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/signal",
"version": "2026.3.9",
"version": "2026.3.8",
"private": true,
"description": "OpenClaw Signal channel plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/slack",
"version": "2026.3.9",
"version": "2026.3.8",
"private": true,
"description": "OpenClaw Slack channel plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/synology-chat",
"version": "2026.3.9",
"version": "2026.3.8",
"description": "Synology Chat channel plugin for OpenClaw",
"type": "module",
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/telegram",
"version": "2026.3.9",
"version": "2026.3.8",
"private": true,
"description": "OpenClaw Telegram channel plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/tlon",
"version": "2026.3.9",
"version": "2026.3.8",
"description": "OpenClaw Tlon/Urbit channel plugin",
"type": "module",
"dependencies": {

View File

@@ -1,17 +1,5 @@
# Changelog
## 2026.3.9
### Changes
- Version alignment with core OpenClaw release numbers.
## 2026.3.8-beta.1
### Changes
- Version alignment with core OpenClaw release numbers.
## 2026.3.8
### Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/twitch",
"version": "2026.3.9",
"version": "2026.3.8",
"description": "OpenClaw Twitch channel plugin",
"type": "module",
"dependencies": {

View File

@@ -1,17 +1,5 @@
# Changelog
## 2026.3.9
### Changes
- Version alignment with core OpenClaw release numbers.
## 2026.3.8-beta.1
### Changes
- Version alignment with core OpenClaw release numbers.
## 2026.3.8
### Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/voice-call",
"version": "2026.3.9",
"version": "2026.3.8",
"description": "OpenClaw voice-call plugin",
"type": "module",
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/whatsapp",
"version": "2026.3.9",
"version": "2026.3.8",
"private": true,
"description": "OpenClaw WhatsApp channel plugin",
"type": "module",

View File

@@ -1,17 +1,5 @@
# Changelog
## 2026.3.9
### Changes
- Version alignment with core OpenClaw release numbers.
## 2026.3.8-beta.1
### Changes
- Version alignment with core OpenClaw release numbers.
## 2026.3.8
### Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/zalo",
"version": "2026.3.9",
"version": "2026.3.8",
"description": "OpenClaw Zalo channel plugin",
"type": "module",
"dependencies": {

View File

@@ -1,17 +1,5 @@
# Changelog
## 2026.3.9
### Changes
- Version alignment with core OpenClaw release numbers.
## 2026.3.8-beta.1
### Changes
- Version alignment with core OpenClaw release numbers.
## 2026.3.8
### Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/zalouser",
"version": "2026.3.9",
"version": "2026.3.8",
"description": "OpenClaw Zalo Personal Account plugin via native zca-js integration",
"type": "module",
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "openclaw",
"version": "2026.3.9",
"version": "2026.3.8",
"description": "Multi-channel AI gateway with extensible messaging integrations",
"keywords": [],
"homepage": "https://github.com/openclaw/openclaw#readme",
@@ -344,10 +344,10 @@
"@larksuiteoapi/node-sdk": "^1.59.0",
"@line/bot-sdk": "^10.6.0",
"@lydell/node-pty": "1.2.0-beta.3",
"@mariozechner/pi-agent-core": "0.57.1",
"@mariozechner/pi-ai": "0.57.1",
"@mariozechner/pi-coding-agent": "0.57.1",
"@mariozechner/pi-tui": "0.57.1",
"@mariozechner/pi-agent-core": "0.55.3",
"@mariozechner/pi-ai": "0.55.3",
"@mariozechner/pi-coding-agent": "0.55.3",
"@mariozechner/pi-tui": "0.55.3",
"@mozilla/readability": "^0.6.0",
"@sinclair/typebox": "0.34.48",
"@slack/bolt": "^4.6.0",
@@ -380,7 +380,7 @@
"qrcode-terminal": "^0.12.0",
"sharp": "^0.34.5",
"sqlite-vec": "0.1.7-alpha.2",
"tar": "7.5.11",
"tar": "7.5.10",
"tslog": "^4.10.2",
"undici": "^7.22.0",
"ws": "^8.19.0",
@@ -396,7 +396,7 @@
"@types/node": "^25.3.5",
"@types/qrcode-terminal": "^0.12.2",
"@types/ws": "^8.18.1",
"@typescript/native-preview": "7.0.0-dev.20260308.1",
"@typescript/native-preview": "7.0.0-dev.20260307.1",
"@vitest/coverage-v8": "^4.0.18",
"jscpd": "4.0.8",
"lit": "^3.3.2",

481
pnpm-lock.yaml generated
View File

@@ -58,17 +58,17 @@ importers:
specifier: 1.2.0-beta.3
version: 1.2.0-beta.3
'@mariozechner/pi-agent-core':
specifier: 0.57.1
version: 0.57.1(ws@8.19.0)(zod@4.3.6)
specifier: 0.55.3
version: 0.55.3(ws@8.19.0)(zod@4.3.6)
'@mariozechner/pi-ai':
specifier: 0.57.1
version: 0.57.1(ws@8.19.0)(zod@4.3.6)
specifier: 0.55.3
version: 0.55.3(ws@8.19.0)(zod@4.3.6)
'@mariozechner/pi-coding-agent':
specifier: 0.57.1
version: 0.57.1(ws@8.19.0)(zod@4.3.6)
specifier: 0.55.3
version: 0.55.3(ws@8.19.0)(zod@4.3.6)
'@mariozechner/pi-tui':
specifier: 0.57.1
version: 0.57.1
specifier: 0.55.3
version: 0.55.3
'@mozilla/readability':
specifier: ^0.6.0
version: 0.6.0
@@ -215,8 +215,8 @@ importers:
specifier: ^8.18.1
version: 8.18.1
'@typescript/native-preview':
specifier: 7.0.0-dev.20260308.1
version: 7.0.0-dev.20260308.1
specifier: 7.0.0-dev.20260307.1
version: 7.0.0-dev.20260307.1
'@vitest/coverage-v8':
specifier: ^4.0.18
version: 4.0.18(@vitest/browser@4.0.18(vite@7.3.1(@types/node@25.3.5)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2))(vitest@4.0.18))(vitest@4.0.18)
@@ -240,7 +240,7 @@ importers:
version: 0.21.1(signal-polyfill@0.2.2)
tsdown:
specifier: 0.21.0
version: 0.21.0(@typescript/native-preview@7.0.0-dev.20260308.1)(typescript@5.9.3)
version: 0.21.0(@typescript/native-preview@7.0.0-dev.20260307.1)(typescript@5.9.3)
tsx:
specifier: ^4.21.0
version: 4.21.0
@@ -369,8 +369,8 @@ importers:
extensions/matrix:
dependencies:
'@mariozechner/pi-agent-core':
specifier: 0.57.1
version: 0.57.1(ws@8.19.0)(zod@4.3.6)
specifier: 0.55.3
version: 0.55.3(ws@8.19.0)(zod@4.3.6)
'@matrix-org/matrix-sdk-crypto-nodejs':
specifier: ^0.4.0
version: 0.4.0
@@ -622,10 +622,6 @@ packages:
resolution: {integrity: sha512-GA96wgTFB4Z5vhysm+hErbgiEWZ9JqAl09BxARajL7Oanpf0KvdIjxuLp2rD/XqEIks9yG/5Rh9XIAoCUUTZXw==}
engines: {node: '>=20.0.0'}
'@aws-sdk/client-bedrock-runtime@3.1004.0':
resolution: {integrity: sha512-t8cl+bPLlHZQD2Sw1a4hSLUybqJZU71+m8znkyeU8CHntFqEp2mMbuLKdHKaAYQ1fAApXMsvzenCAkDzNeeJlw==}
engines: {node: '>=20.0.0'}
'@aws-sdk/client-bedrock@3.1000.0':
resolution: {integrity: sha512-wGU8uJXrPW/hZuHdPNVe1kAFIBiKcslBcoDBN0eYBzS13um8p5jJiQJ9WsD1nSpKCmyx7qZXc6xjcbIQPyOrrA==}
engines: {node: '>=20.0.0'}
@@ -714,10 +710,6 @@ packages:
resolution: {integrity: sha512-8aiVJh6fTdl8gcyL+sVNcNwTtWpmoFa1Sh7xlj6Z7L/cZ/tYMEBHq44wTYG8Kt0z/PpGNopD89nbj3FHl9QmTA==}
engines: {node: '>=20.0.0'}
'@aws-sdk/eventstream-handler-node@3.972.10':
resolution: {integrity: sha512-g2Z9s6Y4iNh0wICaEqutgYgt/Pmhv5Ev9G3eKGFe2w9VuZDhc76vYdop6I5OocmpHV79d4TuLG+JWg5rQIVDVA==}
engines: {node: '>=20.0.0'}
'@aws-sdk/eventstream-handler-node@3.972.9':
resolution: {integrity: sha512-mKPiiVssgFDWkAXdEDh8+wpr2pFSX/fBn2onXXnrfIAYbdZhYb4WilKbZ3SJMUnQi+Y48jZMam5J0RrgARluaA==}
engines: {node: '>=20.0.0'}
@@ -730,10 +722,6 @@ packages:
resolution: {integrity: sha512-mB2+3G/oxRC+y9WRk0KCdradE2rSfxxJpcOSmAm+vDh3ex3WQHVLZ1catNIe1j5NQ+3FLBsNMRPVGkZ43PRpjw==}
engines: {node: '>=20.0.0'}
'@aws-sdk/middleware-eventstream@3.972.7':
resolution: {integrity: sha512-VWndapHYCfwLgPpCb/xwlMKG4imhFzKJzZcKOEioGn7OHY+6gdr0K7oqy1HZgbLa3ACznZ9fku+DzmAi8fUC0g==}
engines: {node: '>=20.0.0'}
'@aws-sdk/middleware-expect-continue@3.972.6':
resolution: {integrity: sha512-QMdffpU+GkSGC+bz6WdqlclqIeCsOfgX8JFZ5xvwDtX+UTj4mIXm3uXu7Ko6dBseRcJz1FA6T9OmlAAY6JgJUg==}
engines: {node: '>=20.0.0'}
@@ -790,10 +778,6 @@ packages:
resolution: {integrity: sha512-uNqRpbL6djE+XXO4cQ+P8ra37cxNNBP+2IfkVOXu1xFdGMfW+uOTxBQuDPpP43i40PBRBXK5un79l/oYpbzYkA==}
engines: {node: '>= 14.0.0'}
'@aws-sdk/middleware-websocket@3.972.12':
resolution: {integrity: sha512-iyPP6FVDKe/5wy5ojC0akpDFG1vX3FeCUU47JuwN8xfvT66xlEI8qUJZPtN55TJVFzzWZJpWL78eqUE31md08Q==}
engines: {node: '>= 14.0.0'}
'@aws-sdk/nested-clients@3.996.3':
resolution: {integrity: sha512-AU5TY1V29xqwg/MxmA2odwysTez+ccFAhmfRJk+QZT5HNv90UTA9qKd1J9THlsQkvmH7HWTEV1lDNxkQO5PzNw==}
engines: {node: '>=20.0.0'}
@@ -854,10 +838,6 @@ packages:
resolution: {integrity: sha512-0YNVNgFyziCejXJx0rzxPiD2rkxTWco4c9wiMF6n37Tb9aQvIF8+t7GyEyIFCwQHZ0VMQaAl+nCZHOYz5I5EKw==}
engines: {node: '>=20.0.0'}
'@aws-sdk/util-format-url@3.972.7':
resolution: {integrity: sha512-V+PbnWfUl93GuFwsOHsAq7hY/fnm9kElRqR8IexIJr5Rvif9e614X5sGSyz3mVSf1YAZ+VTy63W1/pGdA55zyA==}
engines: {node: '>=20.0.0'}
'@aws-sdk/util-locate-window@3.965.4':
resolution: {integrity: sha512-H1onv5SkgPBK2P6JR2MjGgbOnttoNzSPIRoeZTNPZYyaplwGg50zS3amXvXqF0/qfXpWEC9rLWU564QTB9bSog==}
engines: {node: '>=20.0.0'}
@@ -1231,15 +1211,6 @@ packages:
'@modelcontextprotocol/sdk':
optional: true
'@google/genai@1.44.0':
resolution: {integrity: sha512-kRt9ZtuXmz+tLlcNntN/VV4LRdpl6ZOu5B1KbfNgfR65db15O6sUQcwnwLka8sT/V6qysD93fWrgJHF2L7dA9A==}
engines: {node: '>=20.0.0'}
peerDependencies:
'@modelcontextprotocol/sdk': ^1.25.2
peerDependenciesMeta:
'@modelcontextprotocol/sdk':
optional: true
'@grammyjs/runner@2.0.3':
resolution: {integrity: sha512-nckmTs1dPWfVQteK9cxqxzE+0m1VRvluLWB8UgFzsjg62w3qthPJt0TYtJBEdG7OedvfQq4vnFAyE6iaMkR42A==}
engines: {node: '>=12.20.0 || >=14.13.1'}
@@ -1648,38 +1619,20 @@ packages:
resolution: {integrity: sha512-rqbfpQ9BrP6BDiW+Ps3A8Z/p9+Md/pAfc/ECq8JP6cwnZL/jQgU355KWZKtF8zM9az1p0Q9hIWi9cQygVo6Auw==}
engines: {node: '>=20.0.0'}
'@mariozechner/pi-agent-core@0.57.1':
resolution: {integrity: sha512-WXsBbkNWOObFGHkhixaT8GXJpHDd3+fn8QntYF+4R8Sa9WB90ENXWidO6b7vcKX+JX0jjO5dIsQxmzosARJKlg==}
engines: {node: '>=20.0.0'}
'@mariozechner/pi-ai@0.55.3':
resolution: {integrity: sha512-f9jWoDzJR9Wy/H8JPMbjoM4WvVUeFZ65QdYA9UHIfoOopDfwWE8F8JHQOj5mmmILMacXuzsqA3J7MYqNWZRvvQ==}
engines: {node: '>=20.0.0'}
hasBin: true
'@mariozechner/pi-ai@0.57.1':
resolution: {integrity: sha512-Bd/J4a3YpdzJVyHLih0vDSdB0QPL4ti0XsAwtHOK/8eVhB0fHM1CpcgIrcBFJ23TMcKXMi0qamz18ERfp8tmgg==}
engines: {node: '>=20.0.0'}
hasBin: true
'@mariozechner/pi-coding-agent@0.55.3':
resolution: {integrity: sha512-5SFbB7/BIp/Crjre7UNjUeNfpoU1KSW/i6LXa+ikJTBqI5LukWq2avE5l0v0M8Pg/dt1go2XCLrNFlQJiQDSPQ==}
engines: {node: '>=20.0.0'}
hasBin: true
'@mariozechner/pi-coding-agent@0.57.1':
resolution: {integrity: sha512-u5MQEduj68rwVIsRsqrWkJYiJCyPph/a6bMoJAQKo1sb+Pc17Y/ojwa+wGssnUMjEB38AQKofWTVe8NFEpSWNw==}
engines: {node: '>=20.6.0'}
hasBin: true
'@mariozechner/pi-tui@0.55.3':
resolution: {integrity: sha512-Gh4wkYgiSPCJJaB/4wEWSL7Ga8bxSq1Crp1RPRT4vKybE/DG0W/MQr5VJDvktarxtJrD16ixScwE4dzdox/PIA==}
engines: {node: '>=20.0.0'}
'@mariozechner/pi-tui@0.57.1':
resolution: {integrity: sha512-cjoRghLbeAHV0tTJeHgZXaryUi5zzBZofeZ7uJun1gztnckLLRjoVeaPTujNlc5BIfyKvFqhh1QWCZng/MXlpg==}
engines: {node: '>=20.0.0'}
'@matrix-org/matrix-sdk-crypto-nodejs@0.4.0':
resolution: {integrity: sha512-+qqgpn39XFSbsD0dFjssGO9vHEP7sTyfs8yTpt8vuqWpUpF20QMwpCZi0jpYw7GxjErNTsMshopuo8677DfGEA==}
engines: {node: '>= 22'}
@@ -1695,9 +1648,6 @@ packages:
'@mistralai/mistralai@1.10.0':
resolution: {integrity: sha512-tdIgWs4Le8vpvPiUEWne6tK0qbVc+jMenujnvTqOjogrJUsCSQhus0tHTU1avDDh5//Rq2dFgP9mWRAdIEoBqg==}
'@mistralai/mistralai@1.14.1':
resolution: {integrity: sha512-IiLmmZFCCTReQgPAT33r7KQ1nYo5JPdvGkrkZqA8qQ2qB1GHgs5LoP5K2ICyrjnpw2n8oSxMM/VP+liiKcGNlQ==}
'@mozilla/readability@0.6.0':
resolution: {integrity: sha512-juG5VWh4qAivzTAeMzvY9xs9HY5rAcr2E4I7tiSSCokRFi7XIZCAu92ZkSTsIj1OPceCifL3cpfteP3pDT9/QQ==}
engines: {node: '>=14.0.0'}
@@ -2850,42 +2800,22 @@ packages:
resolution: {integrity: sha512-A4ynrsFFfSXUHicfTcRehytppFBcY3HQxEGYiyGktPIOye3Ot7fxpiy4VR42WmtGI4Wfo6OXt/c1Ky1nUFxYYQ==}
engines: {node: '>=18.0.0'}
'@smithy/eventstream-codec@4.2.11':
resolution: {integrity: sha512-Sf39Ml0iVX+ba/bgMPxaXWAAFmHqYLTmbjAPfLPLY8CrYkRDEqZdUsKC1OwVMCdJXfAt0v4j49GIJ8DoSYAe6w==}
engines: {node: '>=18.0.0'}
'@smithy/eventstream-serde-browser@4.2.10':
resolution: {integrity: sha512-0xupsu9yj9oDVuQ50YCTS9nuSYhGlrwqdaKQel9y2Fz7LU9fNErVlw9N0o4pm4qqvWEGbSTI4HKc6XJfB30MVw==}
engines: {node: '>=18.0.0'}
'@smithy/eventstream-serde-browser@4.2.11':
resolution: {integrity: sha512-3rEpo3G6f/nRS7fQDsZmxw/ius6rnlIpz4UX6FlALEzz8JoSxFmdBt0SZnthis+km7sQo6q5/3e+UJcuQivoXA==}
engines: {node: '>=18.0.0'}
'@smithy/eventstream-serde-config-resolver@4.3.10':
resolution: {integrity: sha512-8kn6sinrduk0yaYHMJDsNuiFpXwQwibR7n/4CDUqn4UgaG+SeBHu5jHGFdU9BLFAM7Q4/gvr9RYxBHz9/jKrhA==}
engines: {node: '>=18.0.0'}
'@smithy/eventstream-serde-config-resolver@4.3.11':
resolution: {integrity: sha512-XeNIA8tcP/GDWnnKkO7qEm/bg0B/bP9lvIXZBXcGZwZ+VYM8h8k9wuDvUODtdQ2Wcp2RcBkPTCSMmaniVHrMlA==}
engines: {node: '>=18.0.0'}
'@smithy/eventstream-serde-node@4.2.10':
resolution: {integrity: sha512-uUrxPGgIffnYfvIOUmBM5i+USdEBRTdh7mLPttjphgtooxQ8CtdO1p6K5+Q4BBAZvKlvtJ9jWyrWpBJYzBKsyQ==}
engines: {node: '>=18.0.0'}
'@smithy/eventstream-serde-node@4.2.11':
resolution: {integrity: sha512-fzbCh18rscBDTQSCrsp1fGcclLNF//nJyhjldsEl/5wCYmgpHblv5JSppQAyQI24lClsFT0wV06N1Porn0IsEw==}
engines: {node: '>=18.0.0'}
'@smithy/eventstream-serde-universal@4.2.10':
resolution: {integrity: sha512-aArqzOEvcs2dK+xQVCgLbpJQGfZihw8SD4ymhkwNTtwKbnrzdhJsFDKuMQnam2kF69WzgJYOU5eJlCx+CA32bw==}
engines: {node: '>=18.0.0'}
'@smithy/eventstream-serde-universal@4.2.11':
resolution: {integrity: sha512-MJ7HcI+jEkqoWT5vp+uoVaAjBrmxBtKhZTeynDRG/seEjJfqyg3SiqMMqyPnAMzmIfLaeJ/uiuSDP/l9AnMy/Q==}
engines: {node: '>=18.0.0'}
'@smithy/fetch-http-handler@5.3.11':
resolution: {integrity: sha512-wbTRjOxdFuyEg0CpumjZO0hkUl+fetJFqxNROepuLIoijQh51aMBmzFLfoQdwRjxsuuS2jizzIUTjPWgd8pd7g==}
engines: {node: '>=18.0.0'}
@@ -3291,12 +3221,12 @@ packages:
'@swc/helpers@0.5.19':
resolution: {integrity: sha512-QamiFeIK3txNjgUTNppE6MiG3p7TdninpZu0E0PbqVh1a9FNLT2FRhisaa4NcaX52XVhA5l7Pk58Ft7Sqi/2sA==}
'@thi.ng/bitstream@2.4.43':
resolution: {integrity: sha512-tObOEr+osboa0kqQPk7Ny0E3vVfBRch13YJO5RpaDDSkMQmoXK/pw3yW/6kKJIObt27YQol6pGlOZBvB8MsghQ==}
'@thi.ng/bitstream@2.4.41':
resolution: {integrity: sha512-treRzw3+7I1YCuilFtznwT3SGtceS9spUXhyBqeuKNTm4nIfMuvg4fNqx4GgpuS6cGPQNPMUJm0OyzKnSe2Emw==}
engines: {node: '>=18'}
'@thi.ng/errors@2.6.5':
resolution: {integrity: sha512-XKfcJzxikMI1+MKSiABcLzI2WIsm4SxGEdLIIQjYqew3q3CoypGe+w5W/DMvMWF6eFWT6ONINbiJ6QMHFTfVzA==}
'@thi.ng/errors@2.6.3':
resolution: {integrity: sha512-owkOOKHf7MrAPN2jNpKWDdY/vjtPFiJf6oxZ3jkkhV6ICTu2iY1fXIR2wQ7kVEeybdtb0w24k2PtrU43OYCWdg==}
engines: {node: '>=18'}
'@tinyhttp/content-disposition@2.2.4':
@@ -3367,8 +3297,8 @@ packages:
'@tybys/wasm-util@0.10.1':
resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==}
'@types/aws-lambda@8.10.161':
resolution: {integrity: sha512-rUYdp+MQwSFocxIOcSsYSF3YYYC/uUpMbCY/mbO21vGqfrEYvNSoPyKYDj6RhXXpPfS0KstW9RwG3qXh9sL7FQ==}
'@types/aws-lambda@8.10.160':
resolution: {integrity: sha512-uoO4QVQNWFPJMh26pXtmtrRfGshPUSpMZGUyUQY20FhfHEElEBOPKgVmFs1z+kbpyBsRs2JnoOPT7++Z4GA9pA==}
'@types/body-parser@1.19.6':
resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==}
@@ -3448,8 +3378,8 @@ packages:
'@types/node@10.17.60':
resolution: {integrity: sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==}
'@types/node@20.19.37':
resolution: {integrity: sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==}
'@types/node@20.19.35':
resolution: {integrity: sha512-Uarfe6J91b9HAUXxjvSOdiO2UPOKLm07Q1oh0JHxoZ1y8HoqxDAu3gVrsrOHeiio0kSsoVBt4wFrKOm0dKxVPQ==}
'@types/node@24.11.0':
resolution: {integrity: sha512-fPxQqz4VTgPI/IQ+lj9r0h+fDR66bzoeMGHp8ASee+32OSGIkeASsoZuJixsQoVef1QJbeubcPBxKk22QVoWdw==}
@@ -3502,43 +3432,43 @@ packages:
'@types/yauzl@2.10.3':
resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==}
'@typescript/native-preview-darwin-arm64@7.0.0-dev.20260308.1':
resolution: {integrity: sha512-mywkctYr45fUBUYD35poInc9HEjup0zyCO5z3ZU2QC9eCQShpwYSDceoSCwxVKB/b/f/CU6H3LqINFeIz5CvrQ==}
'@typescript/native-preview-darwin-arm64@7.0.0-dev.20260307.1':
resolution: {integrity: sha512-VpnrMP4iDLSTT9Hg/KrHwuIHLZr5dxYPMFErfv3ZDA0tv48u2H1lBhHVVMMopCuskuX3C35EOJbxLkxCJd6zDw==}
cpu: [arm64]
os: [darwin]
'@typescript/native-preview-darwin-x64@7.0.0-dev.20260308.1':
resolution: {integrity: sha512-iF+Y4USbCiD5BxmXI6xYuy+S6d2BhxKDb3YHjchzqg3AgleDNTd2rqSzlWv4ku26V2iOSfpM9t1H/xluL9pgNw==}
'@typescript/native-preview-darwin-x64@7.0.0-dev.20260307.1':
resolution: {integrity: sha512-+4akGPxwfrPy2AYmQO1bp6CXxUVlBPrL0lSv+wY/E8vNGqwF0UtJCwAcR54ae1+k9EmoirT7Xn6LE3Io6mXntg==}
cpu: [x64]
os: [darwin]
'@typescript/native-preview-linux-arm64@7.0.0-dev.20260308.1':
resolution: {integrity: sha512-uEIIbW1JYPGEesVh/P5xA+xox7pQ6toeFPeke2X2H2bs5YkWHVaUQtVZuKNmGelw+2PCG6XRrXvMgMp056ebuQ==}
'@typescript/native-preview-linux-arm64@7.0.0-dev.20260307.1':
resolution: {integrity: sha512-u4kXuHN2p+HeWsnTixoEOwALsCoS+n3/ukWdnV/mwyg6BKuuU69qCv3/miY6YPFtE7mUwzPdflEXsvkZJbJ/RA==}
cpu: [arm64]
os: [linux]
'@typescript/native-preview-linux-arm@7.0.0-dev.20260308.1':
resolution: {integrity: sha512-vg8hwfwIhT8CmYJI5lG3PP8IoNzKKBGbq1cKjxQabSZTPuQKwVFVity2XKTKZKd+qRGL7xW4UWMJZLFgSx3b2Q==}
'@typescript/native-preview-linux-arm@7.0.0-dev.20260307.1':
resolution: {integrity: sha512-E0Pve6BjTVvPiHq9cPVQu6fbW/Qo/CEs1VN2NMILd0xzFVpVd9FIvzV+Ft6pZilu1SBcihThW3sQ92l03Cw2+Q==}
cpu: [arm]
os: [linux]
'@typescript/native-preview-linux-x64@7.0.0-dev.20260308.1':
resolution: {integrity: sha512-Yd/ht0CGE4NYUAjuHa1u4VbiJbyUgvDh+b2o+Zcb2h5t8B761DIzDm24QqVXh+KhvGUoEodXWg3g3APxLHqj8Q==}
'@typescript/native-preview-linux-x64@7.0.0-dev.20260307.1':
resolution: {integrity: sha512-MzuRjTYQIS7XrJcH0As18SbaQU+rFhf9LCpXs2QeHjhXQ33wjuFDNhQeurg2eKm6A0xE0GoW9K+sKsm8bhzzPg==}
cpu: [x64]
os: [linux]
'@typescript/native-preview-win32-arm64@7.0.0-dev.20260308.1':
resolution: {integrity: sha512-Klk6BoiHegfPmkO0YYrXmbYVdPjOfN25lRkzenqDIwbyzPlABHvICCyo5YRvWD3HU4EeDfLisIFU9wEd/0duCw==}
'@typescript/native-preview-win32-arm64@7.0.0-dev.20260307.1':
resolution: {integrity: sha512-UNZl8Q6lx1njEPU8+FNjYvqii5PtDjk6cyxmVPwwJI2Snz5T5qY6oadkUds6CJsLkt7s4UB3P5XgLu1+vwoYGw==}
cpu: [arm64]
os: [win32]
'@typescript/native-preview-win32-x64@7.0.0-dev.20260308.1':
resolution: {integrity: sha512-4LrXmaMfzedwczANIkD/M9guPD4EWuQnCxOJsJkdYi3ExWQDjIFwfmxTtAmfPBWxVExLfn7UUkz/yCtcv2Wd+w==}
'@typescript/native-preview-win32-x64@7.0.0-dev.20260307.1':
resolution: {integrity: sha512-aPJb4v0Df9GzWFWbO4YbLg0OjmjxZgXngkF1M746r4CgOdydWgosNPWypzzAwiliGKvCLwfAWYiV+T5Jf1vQ3g==}
cpu: [x64]
os: [win32]
'@typescript/native-preview@7.0.0-dev.20260308.1':
resolution: {integrity: sha512-8a3oe5IAfBkEfMouRheNhOXUScBSHIUknPvUdsbxx7s+Ja1lxFNA1X1TTl2T18vu72Q/mM86vxefw5eW8/ps3g==}
'@typescript/native-preview@7.0.0-dev.20260307.1':
resolution: {integrity: sha512-NcKdPiGjxxxdh7fLgRKTrn5hLntbt89NOodNaSrMChTfJwvLaDkgrRlnO7v5x+m7nQc87Qf1y7UoT1ZEZUBB4Q==}
hasBin: true
'@typespec/ts-http-runtime@0.3.3':
@@ -3895,8 +3825,8 @@ packages:
bowser@2.14.1:
resolution: {integrity: sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==}
brace-expansion@5.0.4:
resolution: {integrity: sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==}
brace-expansion@5.0.3:
resolution: {integrity: sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==}
engines: {node: 18 || 20 || >=22}
braces@3.0.3:
@@ -4051,8 +3981,8 @@ packages:
resolution: {integrity: sha512-H4UfQhZyakIjC74I9d34fGYDwk3XpSr17QhEd0Q3I9Xq1CETHo4Hcuo87WyWHpAF1aSLjLRf5lD9ZGX2qStUvg==}
engines: {node: '>=4.0.0'}
command-line-usage@7.0.4:
resolution: {integrity: sha512-85UdvzTNx/+s5CkSgBm/0hzP80RFHAa7PsfeADE5ezZF3uHz3/Tqj9gIKGT9PTtpycc3Ua64T0oVulGfKxzfqg==}
command-line-usage@7.0.3:
resolution: {integrity: sha512-PqMLy5+YGwhMh1wS04mVG44oqDsgyLRSKJBdOo1bnYhMKBW65gZF1dRp2OZRhiTjgUHljy99qkO7bsctLaw35Q==}
engines: {node: '>=12.20.0'}
commander@10.0.1:
@@ -4512,10 +4442,6 @@ packages:
resolution: {integrity: sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==}
engines: {node: '>=14.14'}
fs-extra@11.3.4:
resolution: {integrity: sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==}
engines: {node: '>=14.14'}
fs.realpath@1.0.0:
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
@@ -4912,8 +4838,8 @@ packages:
json-stringify-safe@5.0.1:
resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==}
json-with-bigint@3.5.7:
resolution: {integrity: sha512-7ei3MdAI5+fJPVnKlW77TKNKwQ5ppSzWvhPuSuINT/GYW9ZOC1eRKOuhV9yHG5aEsUPj9BBx5JIekkmoLHxZOw==}
json-with-bigint@3.5.3:
resolution: {integrity: sha512-QObKu6nxy7NsxqR0VK4rkXnsNr5L9ElJaGEg+ucJ6J7/suoKZ0n+p76cu9aCqowytxEbwYNzvrMerfMkXneF5A==}
json5@2.2.3:
resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==}
@@ -5334,8 +5260,8 @@ packages:
resolution: {integrity: sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==}
engines: {node: '>= 0.4.0'}
node-addon-api@8.6.0:
resolution: {integrity: sha512-gBVjCaqDlRUk0EwoPNKzIr9KkS9041G/q31IBShPs1Xz6UTA+EXdZADbzqAJQrpDRq71CIMnOP5VMut3SL0z5Q==}
node-addon-api@8.5.0:
resolution: {integrity: sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==}
engines: {node: ^18 || ^20 || >= 21}
node-api-headers@1.8.0:
@@ -5478,18 +5404,6 @@ packages:
zod:
optional: true
openai@6.26.0:
resolution: {integrity: sha512-zd23dbWTjiJ6sSAX6s0HrCZi41JwTA1bQVs0wLQPZ2/5o2gxOJA5wh7yOAUgwYybfhDXyhwlpeQf7Mlgx8EOCA==}
hasBin: true
peerDependencies:
ws: ^8.18.0
zod: ^3.25 || ^4.0
peerDependenciesMeta:
ws:
optional: true
zod:
optional: true
openai@6.27.0:
resolution: {integrity: sha512-osTKySlrdYrLYTt0zjhY8yp0JUBmWDCN+Q+QxsV4xMQnnoVFpylgKGgxwN8sSdTNw0G4y+WUXs4eCMWpyDNWZQ==}
hasBin: true
@@ -6870,58 +6784,6 @@ snapshots:
transitivePeerDependencies:
- aws-crt
'@aws-sdk/client-bedrock-runtime@3.1004.0':
dependencies:
'@aws-crypto/sha256-browser': 5.2.0
'@aws-crypto/sha256-js': 5.2.0
'@aws-sdk/core': 3.973.18
'@aws-sdk/credential-provider-node': 3.972.18
'@aws-sdk/eventstream-handler-node': 3.972.10
'@aws-sdk/middleware-eventstream': 3.972.7
'@aws-sdk/middleware-host-header': 3.972.7
'@aws-sdk/middleware-logger': 3.972.7
'@aws-sdk/middleware-recursion-detection': 3.972.7
'@aws-sdk/middleware-user-agent': 3.972.19
'@aws-sdk/middleware-websocket': 3.972.12
'@aws-sdk/region-config-resolver': 3.972.7
'@aws-sdk/token-providers': 3.1004.0
'@aws-sdk/types': 3.973.5
'@aws-sdk/util-endpoints': 3.996.4
'@aws-sdk/util-user-agent-browser': 3.972.7
'@aws-sdk/util-user-agent-node': 3.973.4
'@smithy/config-resolver': 4.4.10
'@smithy/core': 3.23.9
'@smithy/eventstream-serde-browser': 4.2.11
'@smithy/eventstream-serde-config-resolver': 4.3.11
'@smithy/eventstream-serde-node': 4.2.11
'@smithy/fetch-http-handler': 5.3.13
'@smithy/hash-node': 4.2.11
'@smithy/invalid-dependency': 4.2.11
'@smithy/middleware-content-length': 4.2.11
'@smithy/middleware-endpoint': 4.4.23
'@smithy/middleware-retry': 4.4.40
'@smithy/middleware-serde': 4.2.12
'@smithy/middleware-stack': 4.2.11
'@smithy/node-config-provider': 4.3.11
'@smithy/node-http-handler': 4.4.14
'@smithy/protocol-http': 5.3.11
'@smithy/smithy-client': 4.12.3
'@smithy/types': 4.13.0
'@smithy/url-parser': 4.2.11
'@smithy/util-base64': 4.3.2
'@smithy/util-body-length-browser': 4.2.2
'@smithy/util-body-length-node': 4.2.3
'@smithy/util-defaults-mode-browser': 4.3.39
'@smithy/util-defaults-mode-node': 4.2.42
'@smithy/util-endpoints': 3.3.2
'@smithy/util-middleware': 4.2.11
'@smithy/util-retry': 4.2.11
'@smithy/util-stream': 4.5.17
'@smithy/util-utf8': 4.2.2
tslib: 2.8.1
transitivePeerDependencies:
- aws-crt
'@aws-sdk/client-bedrock@3.1000.0':
dependencies:
'@aws-crypto/sha256-browser': 5.2.0
@@ -7317,13 +7179,6 @@ snapshots:
transitivePeerDependencies:
- aws-crt
'@aws-sdk/eventstream-handler-node@3.972.10':
dependencies:
'@aws-sdk/types': 3.973.5
'@smithy/eventstream-codec': 4.2.11
'@smithy/types': 4.13.0
tslib: 2.8.1
'@aws-sdk/eventstream-handler-node@3.972.9':
dependencies:
'@aws-sdk/types': 3.973.4
@@ -7348,13 +7203,6 @@ snapshots:
'@smithy/types': 4.13.0
tslib: 2.8.1
'@aws-sdk/middleware-eventstream@3.972.7':
dependencies:
'@aws-sdk/types': 3.973.5
'@smithy/protocol-http': 5.3.11
'@smithy/types': 4.13.0
tslib: 2.8.1
'@aws-sdk/middleware-expect-continue@3.972.6':
dependencies:
'@aws-sdk/types': 3.973.4
@@ -7486,21 +7334,6 @@ snapshots:
'@smithy/util-utf8': 4.2.1
tslib: 2.8.1
'@aws-sdk/middleware-websocket@3.972.12':
dependencies:
'@aws-sdk/types': 3.973.5
'@aws-sdk/util-format-url': 3.972.7
'@smithy/eventstream-codec': 4.2.11
'@smithy/eventstream-serde-browser': 4.2.11
'@smithy/fetch-http-handler': 5.3.13
'@smithy/protocol-http': 5.3.11
'@smithy/signature-v4': 5.3.11
'@smithy/types': 4.13.0
'@smithy/util-base64': 4.3.2
'@smithy/util-hex-encoding': 4.2.2
'@smithy/util-utf8': 4.2.2
tslib: 2.8.1
'@aws-sdk/nested-clients@3.996.3':
dependencies:
'@aws-crypto/sha256-browser': 5.2.0
@@ -7696,13 +7529,6 @@ snapshots:
'@smithy/types': 4.13.0
tslib: 2.8.1
'@aws-sdk/util-format-url@3.972.7':
dependencies:
'@aws-sdk/types': 3.973.5
'@smithy/querystring-builder': 4.2.11
'@smithy/types': 4.13.0
tslib: 2.8.1
'@aws-sdk/util-locate-window@3.965.4':
dependencies:
tslib: 2.8.1
@@ -7982,7 +7808,7 @@ snapshots:
'@discordjs/opus@0.10.0':
dependencies:
'@discordjs/node-pre-gyp': 0.4.5
node-addon-api: 8.6.0
node-addon-api: 8.5.0
transitivePeerDependencies:
- encoding
- supports-color
@@ -8111,17 +7937,6 @@ snapshots:
- supports-color
- utf-8-validate
'@google/genai@1.44.0':
dependencies:
google-auth-library: 10.6.1
p-retry: 4.6.2
protobufjs: 7.5.4
ws: 8.19.0
transitivePeerDependencies:
- bufferutil
- supports-color
- utf-8-validate
'@grammyjs/runner@2.0.3(grammy@1.41.0)':
dependencies:
abort-controller: 3.0.0
@@ -8513,18 +8328,6 @@ snapshots:
- ws
- zod
'@mariozechner/pi-agent-core@0.57.1(ws@8.19.0)(zod@4.3.6)':
dependencies:
'@mariozechner/pi-ai': 0.57.1(ws@8.19.0)(zod@4.3.6)
transitivePeerDependencies:
- '@modelcontextprotocol/sdk'
- aws-crt
- bufferutil
- supports-color
- utf-8-validate
- ws
- zod
'@mariozechner/pi-ai@0.55.3(ws@8.19.0)(zod@4.3.6)':
dependencies:
'@anthropic-ai/sdk': 0.73.0(zod@4.3.6)
@@ -8549,30 +8352,6 @@ snapshots:
- ws
- zod
'@mariozechner/pi-ai@0.57.1(ws@8.19.0)(zod@4.3.6)':
dependencies:
'@anthropic-ai/sdk': 0.73.0(zod@4.3.6)
'@aws-sdk/client-bedrock-runtime': 3.1004.0
'@google/genai': 1.44.0
'@mistralai/mistralai': 1.14.1
'@sinclair/typebox': 0.34.48
ajv: 8.18.0
ajv-formats: 3.0.1(ajv@8.18.0)
chalk: 5.6.2
openai: 6.26.0(ws@8.19.0)(zod@4.3.6)
partial-json: 0.1.7
proxy-agent: 6.5.0
undici: 7.22.0
zod-to-json-schema: 3.25.1(zod@4.3.6)
transitivePeerDependencies:
- '@modelcontextprotocol/sdk'
- aws-crt
- bufferutil
- supports-color
- utf-8-validate
- ws
- zod
'@mariozechner/pi-coding-agent@0.55.3(ws@8.19.0)(zod@4.3.6)':
dependencies:
'@mariozechner/jiti': 2.6.5
@@ -8604,38 +8383,6 @@ snapshots:
- ws
- zod
'@mariozechner/pi-coding-agent@0.57.1(ws@8.19.0)(zod@4.3.6)':
dependencies:
'@mariozechner/jiti': 2.6.5
'@mariozechner/pi-agent-core': 0.57.1(ws@8.19.0)(zod@4.3.6)
'@mariozechner/pi-ai': 0.57.1(ws@8.19.0)(zod@4.3.6)
'@mariozechner/pi-tui': 0.57.1
'@silvia-odwyer/photon-node': 0.3.4
chalk: 5.6.2
cli-highlight: 2.1.11
diff: 8.0.3
extract-zip: 2.0.1
file-type: 21.3.0
glob: 13.0.6
hosted-git-info: 9.0.2
ignore: 7.0.5
marked: 15.0.12
minimatch: 10.2.4
proper-lockfile: 4.1.2
strip-ansi: 7.2.0
undici: 7.22.0
yaml: 2.8.2
optionalDependencies:
'@mariozechner/clipboard': 0.3.2
transitivePeerDependencies:
- '@modelcontextprotocol/sdk'
- aws-crt
- bufferutil
- supports-color
- utf-8-validate
- ws
- zod
'@mariozechner/pi-tui@0.55.3':
dependencies:
'@types/mime-types': 2.1.4
@@ -8645,16 +8392,6 @@ snapshots:
marked: 15.0.12
mime-types: 3.0.2
'@mariozechner/pi-tui@0.57.1':
dependencies:
'@types/mime-types': 2.1.4
chalk: 5.6.2
get-east-asian-width: 1.5.0
marked: 15.0.12
mime-types: 3.0.2
optionalDependencies:
koffi: 2.15.1
'@matrix-org/matrix-sdk-crypto-nodejs@0.4.0':
dependencies:
https-proxy-agent: 7.0.6
@@ -8689,15 +8426,6 @@ snapshots:
zod: 3.25.76
zod-to-json-schema: 3.25.1(zod@3.25.76)
'@mistralai/mistralai@1.14.1':
dependencies:
ws: 8.19.0
zod: 4.3.6
zod-to-json-schema: 3.25.1(zod@4.3.6)
transitivePeerDependencies:
- bufferutil
- utf-8-validate
'@mozilla/readability@0.6.0': {}
'@napi-rs/canvas-android-arm64@0.1.95':
@@ -8897,7 +8625,7 @@ snapshots:
'@octokit/core': 7.0.6
'@octokit/oauth-authorization-url': 8.0.0
'@octokit/oauth-methods': 6.0.2
'@types/aws-lambda': 8.10.161
'@types/aws-lambda': 8.10.160
universal-user-agent: 7.0.3
'@octokit/oauth-authorization-url@8.0.0': {}
@@ -8950,7 +8678,7 @@ snapshots:
'@octokit/request-error': 7.1.0
'@octokit/types': 16.0.0
fast-content-type-parse: 3.0.0
json-with-bigint: 3.5.7
json-with-bigint: 3.5.3
universal-user-agent: 7.0.3
'@octokit/types@16.0.0':
@@ -9753,59 +9481,29 @@ snapshots:
'@smithy/util-hex-encoding': 4.2.1
tslib: 2.8.1
'@smithy/eventstream-codec@4.2.11':
dependencies:
'@aws-crypto/crc32': 5.2.0
'@smithy/types': 4.13.0
'@smithy/util-hex-encoding': 4.2.2
tslib: 2.8.1
'@smithy/eventstream-serde-browser@4.2.10':
dependencies:
'@smithy/eventstream-serde-universal': 4.2.10
'@smithy/types': 4.13.0
tslib: 2.8.1
'@smithy/eventstream-serde-browser@4.2.11':
dependencies:
'@smithy/eventstream-serde-universal': 4.2.11
'@smithy/types': 4.13.0
tslib: 2.8.1
'@smithy/eventstream-serde-config-resolver@4.3.10':
dependencies:
'@smithy/types': 4.13.0
tslib: 2.8.1
'@smithy/eventstream-serde-config-resolver@4.3.11':
dependencies:
'@smithy/types': 4.13.0
tslib: 2.8.1
'@smithy/eventstream-serde-node@4.2.10':
dependencies:
'@smithy/eventstream-serde-universal': 4.2.10
'@smithy/types': 4.13.0
tslib: 2.8.1
'@smithy/eventstream-serde-node@4.2.11':
dependencies:
'@smithy/eventstream-serde-universal': 4.2.11
'@smithy/types': 4.13.0
tslib: 2.8.1
'@smithy/eventstream-serde-universal@4.2.10':
dependencies:
'@smithy/eventstream-codec': 4.2.10
'@smithy/types': 4.13.0
tslib: 2.8.1
'@smithy/eventstream-serde-universal@4.2.11':
dependencies:
'@smithy/eventstream-codec': 4.2.11
'@smithy/types': 4.13.0
tslib: 2.8.1
'@smithy/fetch-http-handler@5.3.11':
dependencies:
'@smithy/protocol-http': 5.3.10
@@ -10358,12 +10056,12 @@ snapshots:
dependencies:
tslib: 2.8.1
'@thi.ng/bitstream@2.4.43':
'@thi.ng/bitstream@2.4.41':
dependencies:
'@thi.ng/errors': 2.6.5
'@thi.ng/errors': 2.6.3
optional: true
'@thi.ng/errors@2.6.5':
'@thi.ng/errors@2.6.3':
optional: true
'@tinyhttp/content-disposition@2.2.4': {}
@@ -10474,7 +10172,7 @@ snapshots:
tslib: 2.8.1
optional: true
'@types/aws-lambda@8.10.161': {}
'@types/aws-lambda@8.10.160': {}
'@types/body-parser@1.19.6':
dependencies:
@@ -10568,7 +10266,7 @@ snapshots:
'@types/node@10.17.60': {}
'@types/node@20.19.37':
'@types/node@20.19.35':
dependencies:
undici-types: 6.21.0
@@ -10632,36 +10330,36 @@ snapshots:
'@types/node': 25.3.5
optional: true
'@typescript/native-preview-darwin-arm64@7.0.0-dev.20260308.1':
'@typescript/native-preview-darwin-arm64@7.0.0-dev.20260307.1':
optional: true
'@typescript/native-preview-darwin-x64@7.0.0-dev.20260308.1':
'@typescript/native-preview-darwin-x64@7.0.0-dev.20260307.1':
optional: true
'@typescript/native-preview-linux-arm64@7.0.0-dev.20260308.1':
'@typescript/native-preview-linux-arm64@7.0.0-dev.20260307.1':
optional: true
'@typescript/native-preview-linux-arm@7.0.0-dev.20260308.1':
'@typescript/native-preview-linux-arm@7.0.0-dev.20260307.1':
optional: true
'@typescript/native-preview-linux-x64@7.0.0-dev.20260308.1':
'@typescript/native-preview-linux-x64@7.0.0-dev.20260307.1':
optional: true
'@typescript/native-preview-win32-arm64@7.0.0-dev.20260308.1':
'@typescript/native-preview-win32-arm64@7.0.0-dev.20260307.1':
optional: true
'@typescript/native-preview-win32-x64@7.0.0-dev.20260308.1':
'@typescript/native-preview-win32-x64@7.0.0-dev.20260307.1':
optional: true
'@typescript/native-preview@7.0.0-dev.20260308.1':
'@typescript/native-preview@7.0.0-dev.20260307.1':
optionalDependencies:
'@typescript/native-preview-darwin-arm64': 7.0.0-dev.20260308.1
'@typescript/native-preview-darwin-x64': 7.0.0-dev.20260308.1
'@typescript/native-preview-linux-arm': 7.0.0-dev.20260308.1
'@typescript/native-preview-linux-arm64': 7.0.0-dev.20260308.1
'@typescript/native-preview-linux-x64': 7.0.0-dev.20260308.1
'@typescript/native-preview-win32-arm64': 7.0.0-dev.20260308.1
'@typescript/native-preview-win32-x64': 7.0.0-dev.20260308.1
'@typescript/native-preview-darwin-arm64': 7.0.0-dev.20260307.1
'@typescript/native-preview-darwin-x64': 7.0.0-dev.20260307.1
'@typescript/native-preview-linux-arm': 7.0.0-dev.20260307.1
'@typescript/native-preview-linux-arm64': 7.0.0-dev.20260307.1
'@typescript/native-preview-linux-x64': 7.0.0-dev.20260307.1
'@typescript/native-preview-win32-arm64': 7.0.0-dev.20260307.1
'@typescript/native-preview-win32-x64': 7.0.0-dev.20260307.1
'@typespec/ts-http-runtime@0.3.3':
dependencies:
@@ -10915,9 +10613,9 @@ snapshots:
'@swc/helpers': 0.5.19
'@types/command-line-args': 5.2.3
'@types/command-line-usage': 5.0.4
'@types/node': 20.19.37
'@types/node': 20.19.35
command-line-args: 5.2.1
command-line-usage: 7.0.4
command-line-usage: 7.0.3
flatbuffers: 24.12.23
json-bignum: 0.0.3
tslib: 2.8.1
@@ -11087,7 +10785,7 @@ snapshots:
bowser@2.14.1: {}
brace-expansion@5.0.4:
brace-expansion@5.0.3:
dependencies:
balanced-match: 4.0.4
@@ -11210,7 +10908,7 @@ snapshots:
cmake-js@8.0.0:
dependencies:
debug: 4.4.3
fs-extra: 11.3.4
fs-extra: 11.3.3
node-api-headers: 1.8.0
rc: 1.2.8
semver: 7.7.4
@@ -11248,7 +10946,7 @@ snapshots:
lodash.camelcase: 4.3.0
typical: 4.0.0
command-line-usage@7.0.4:
command-line-usage@7.0.3:
dependencies:
array-back: 6.2.2
chalk-template: 0.4.0
@@ -11737,12 +11435,6 @@ snapshots:
jsonfile: 6.2.0
universalify: 2.0.1
fs-extra@11.3.4:
dependencies:
graceful-fs: 4.2.11
jsonfile: 6.2.0
universalify: 2.0.1
fs.realpath@1.0.0:
optional: true
@@ -12070,7 +11762,7 @@ snapshots:
commander: 10.0.1
eventemitter3: 5.0.4
filenamify: 6.0.0
fs-extra: 11.3.4
fs-extra: 11.3.3
is-unicode-supported: 2.1.0
lifecycle-utils: 2.1.0
lodash.debounce: 4.0.8
@@ -12218,7 +11910,7 @@ snapshots:
json-stringify-safe@5.0.1: {}
json-with-bigint@3.5.7: {}
json-with-bigint@3.5.3: {}
json5@2.2.3: {}
@@ -12557,7 +12249,7 @@ snapshots:
minimatch@10.2.4:
dependencies:
brace-expansion: 5.0.4
brace-expansion: 5.0.3
minimist@1.2.8: {}
@@ -12623,7 +12315,7 @@ snapshots:
netmask@2.0.2: {}
node-addon-api@8.6.0: {}
node-addon-api@8.5.0: {}
node-api-headers@1.8.0: {}
@@ -12660,14 +12352,14 @@ snapshots:
cross-spawn: 7.0.6
env-var: 7.5.0
filenamify: 6.0.0
fs-extra: 11.3.4
fs-extra: 11.3.3
ignore: 7.0.5
ipull: 3.9.5
is-unicode-supported: 2.1.0
lifecycle-utils: 3.1.1
log-symbols: 7.0.1
nanoid: 5.1.6
node-addon-api: 8.6.0
node-addon-api: 8.5.0
octokit: 5.0.5
ora: 9.3.0
pretty-ms: 9.3.0
@@ -12811,11 +12503,6 @@ snapshots:
ws: 8.19.0
zod: 4.3.6
openai@6.26.0(ws@8.19.0)(zod@4.3.6):
optionalDependencies:
ws: 8.19.0
zod: 4.3.6
openai@6.27.0(ws@8.19.0)(zod@4.3.6):
optionalDependencies:
ws: 8.19.0
@@ -13300,7 +12987,7 @@ snapshots:
qoa-format@1.0.1:
dependencies:
'@thi.ng/bitstream': 2.4.43
'@thi.ng/bitstream': 2.4.41
optional: true
qrcode-terminal@0.12.0: {}
@@ -13430,7 +13117,7 @@ snapshots:
dependencies:
glob: 10.5.0
rolldown-plugin-dts@0.22.4(@typescript/native-preview@7.0.0-dev.20260308.1)(rolldown@1.0.0-rc.7)(typescript@5.9.3):
rolldown-plugin-dts@0.22.4(@typescript/native-preview@7.0.0-dev.20260307.1)(rolldown@1.0.0-rc.7)(typescript@5.9.3):
dependencies:
'@babel/generator': 8.0.0-rc.2
'@babel/helper-validator-identifier': 8.0.0-rc.2
@@ -13443,7 +13130,7 @@ snapshots:
obug: 2.1.1
rolldown: 1.0.0-rc.7
optionalDependencies:
'@typescript/native-preview': 7.0.0-dev.20260308.1
'@typescript/native-preview': 7.0.0-dev.20260307.1
typescript: 5.9.3
transitivePeerDependencies:
- oxc-resolver
@@ -13978,7 +13665,7 @@ snapshots:
ts-algebra@2.0.0: {}
tsdown@0.21.0(@typescript/native-preview@7.0.0-dev.20260308.1)(typescript@5.9.3):
tsdown@0.21.0(@typescript/native-preview@7.0.0-dev.20260307.1)(typescript@5.9.3):
dependencies:
ansis: 4.2.0
cac: 7.0.0
@@ -13989,7 +13676,7 @@ snapshots:
obug: 2.1.1
picomatch: 4.0.3
rolldown: 1.0.0-rc.7
rolldown-plugin-dts: 0.22.4(@typescript/native-preview@7.0.0-dev.20260308.1)(rolldown@1.0.0-rc.7)(typescript@5.9.3)
rolldown-plugin-dts: 0.22.4(@typescript/native-preview@7.0.0-dev.20260307.1)(rolldown@1.0.0-rc.7)(typescript@5.9.3)
semver: 7.7.4
tinyexec: 1.0.2
tinyglobby: 0.2.15

View File

@@ -104,11 +104,11 @@ const hostMemoryGiB = Math.floor(os.totalmem() / 1024 ** 3);
const highMemLocalHost = !isCI && hostMemoryGiB >= 96;
const lowMemLocalHost = !isCI && hostMemoryGiB < 64;
const nodeMajor = Number.parseInt(process.versions.node.split(".")[0] ?? "", 10);
// vmForks is a big win for transform/import heavy suites, but Node 24+
// regressed with Vitest's vm runtime in this repo, and low-memory local hosts
// vmForks is a big win for transform/import heavy suites, but Node 24 had
// regressions with Vitest's vm runtime in this repo, and low-memory local hosts
// are more likely to hit per-worker V8 heap ceilings. Keep it opt-out via
// OPENCLAW_TEST_VM_FORKS=0, and let users force-enable with =1.
const supportsVmForks = Number.isFinite(nodeMajor) ? nodeMajor < 24 : true;
const supportsVmForks = Number.isFinite(nodeMajor) ? nodeMajor !== 24 : true;
const useVmForks =
process.env.OPENCLAW_TEST_VM_FORKS === "1" ||
(process.env.OPENCLAW_TEST_VM_FORKS !== "0" && !isWindows && supportsVmForks && !lowMemLocalHost);

View File

@@ -1,61 +0,0 @@
import type { SetSessionModeRequest } from "@agentclientprotocol/sdk";
import { describe, expect, it, vi } from "vitest";
import type { GatewayClient } from "../gateway/client.js";
import { createInMemorySessionStore } from "./session.js";
import { AcpGatewayAgent } from "./translator.js";
import { createAcpConnection, createAcpGateway } from "./translator.test-helpers.js";
function createSetSessionModeRequest(modeId: string): SetSessionModeRequest {
return {
sessionId: "session-1",
modeId,
} as unknown as SetSessionModeRequest;
}
function createAgentWithSession(request: GatewayClient["request"]) {
const sessionStore = createInMemorySessionStore();
sessionStore.createSession({
sessionId: "session-1",
sessionKey: "agent:main:main",
cwd: "/tmp",
});
return new AcpGatewayAgent(createAcpConnection(), createAcpGateway(request), {
sessionStore,
});
}
describe("acp setSessionMode", () => {
it("setSessionMode propagates gateway error", async () => {
const request = vi.fn(async () => {
throw new Error("gateway rejected mode change");
}) as GatewayClient["request"];
const agent = createAgentWithSession(request);
await expect(agent.setSessionMode(createSetSessionModeRequest("high"))).rejects.toThrow(
"gateway rejected mode change",
);
expect(request).toHaveBeenCalledWith("sessions.patch", {
key: "agent:main:main",
thinkingLevel: "high",
});
});
it("setSessionMode succeeds when gateway accepts", async () => {
const request = vi.fn(async () => ({ ok: true })) as GatewayClient["request"];
const agent = createAgentWithSession(request);
await expect(agent.setSessionMode(createSetSessionModeRequest("low"))).resolves.toEqual({});
expect(request).toHaveBeenCalledWith("sessions.patch", {
key: "agent:main:main",
thinkingLevel: "low",
});
});
it("setSessionMode returns early for empty modeId", async () => {
const request = vi.fn(async () => ({ ok: true })) as GatewayClient["request"];
const agent = createAgentWithSession(request);
await expect(agent.setSessionMode(createSetSessionModeRequest(""))).resolves.toEqual({});
expect(request).not.toHaveBeenCalled();
});
});

View File

@@ -1,111 +0,0 @@
import type { PromptRequest } from "@agentclientprotocol/sdk";
import { describe, expect, it, vi } from "vitest";
import type { GatewayClient } from "../gateway/client.js";
import type { EventFrame } from "../gateway/protocol/index.js";
import { createInMemorySessionStore } from "./session.js";
import { AcpGatewayAgent } from "./translator.js";
import { createAcpConnection, createAcpGateway } from "./translator.test-helpers.js";
type PendingPromptHarness = {
agent: AcpGatewayAgent;
promptPromise: ReturnType<AcpGatewayAgent["prompt"]>;
runId: string;
};
async function createPendingPromptHarness(): Promise<PendingPromptHarness> {
const sessionId = "session-1";
const sessionKey = "agent:main:main";
let runId: string | undefined;
const request = vi.fn(async (method: string, params?: Record<string, unknown>) => {
if (method === "chat.send") {
runId = params?.idempotencyKey as string | undefined;
return new Promise<never>(() => {});
}
return {};
}) as GatewayClient["request"];
const sessionStore = createInMemorySessionStore();
sessionStore.createSession({
sessionId,
sessionKey,
cwd: "/tmp",
});
const agent = new AcpGatewayAgent(
createAcpConnection(),
createAcpGateway(request as unknown as GatewayClient["request"]),
{ sessionStore },
);
const promptPromise = agent.prompt({
sessionId,
prompt: [{ type: "text", text: "hello" }],
_meta: {},
} as unknown as PromptRequest);
await vi.waitFor(() => {
expect(runId).toBeDefined();
});
return {
agent,
promptPromise,
runId: runId!,
};
}
function createChatEvent(payload: Record<string, unknown>): EventFrame {
return {
type: "event",
event: "chat",
payload,
} as EventFrame;
}
describe("acp translator stop reason mapping", () => {
it("error state resolves as end_turn, not refusal", async () => {
const { agent, promptPromise, runId } = await createPendingPromptHarness();
await agent.handleGatewayEvent(
createChatEvent({
runId,
sessionKey: "agent:main:main",
seq: 1,
state: "error",
errorMessage: "gateway timeout",
}),
);
await expect(promptPromise).resolves.toEqual({ stopReason: "end_turn" });
});
it("error state with no errorMessage resolves as end_turn", async () => {
const { agent, promptPromise, runId } = await createPendingPromptHarness();
await agent.handleGatewayEvent(
createChatEvent({
runId,
sessionKey: "agent:main:main",
seq: 1,
state: "error",
}),
);
await expect(promptPromise).resolves.toEqual({ stopReason: "end_turn" });
});
it("aborted state resolves as cancelled", async () => {
const { agent, promptPromise, runId } = await createPendingPromptHarness();
await agent.handleGatewayEvent(
createChatEvent({
runId,
sessionKey: "agent:main:main",
seq: 1,
state: "aborted",
}),
);
await expect(promptPromise).resolves.toEqual({ stopReason: "cancelled" });
});
});

View File

@@ -256,7 +256,6 @@ export class AcpGatewayAgent implements Agent {
this.log(`setSessionMode: ${session.sessionId} -> ${params.modeId}`);
} catch (err) {
this.log(`setSessionMode error: ${String(err)}`);
throw err;
}
return {};
}
@@ -474,11 +473,7 @@ export class AcpGatewayAgent implements Agent {
return;
}
if (state === "error") {
// ACP has no explicit "server_error" stop reason. Use "end_turn" so clients
// do not treat transient backend errors (timeouts, rate-limits) as deliberate
// refusals. TODO: when ChatEventSchema gains a structured errorKind field
// (e.g. "refusal" | "timeout" | "rate_limit"), use it to distinguish here.
this.finishPrompt(pending.sessionId, pending, "end_turn");
this.finishPrompt(pending.sessionId, pending, "refusal");
}
}

View File

@@ -28,8 +28,8 @@ describe("createAnthropicPayloadLogger", () => {
},
],
};
const streamFn: StreamFn = ((model, __, options) => {
options?.onPayload?.(payload, model);
const streamFn: StreamFn = ((_, __, options) => {
options?.onPayload?.(payload);
return {} as never;
}) as StreamFn;

View File

@@ -136,7 +136,7 @@ export function createAnthropicPayloadLogger(params: {
if (!isAnthropicModel(model)) {
return streamFn(model, context, options);
}
const nextOnPayload = (payload: unknown, payloadModel: Parameters<StreamFn>[0]) => {
const nextOnPayload = (payload: unknown) => {
const redactedPayload = redactImageDataForDiagnostics(payload);
record({
...base,
@@ -145,7 +145,7 @@ export function createAnthropicPayloadLogger(params: {
payload: redactedPayload,
payloadDigest: digest(redactedPayload),
});
return options?.onPayload?.(payload, payloadModel);
options?.onPayload?.(payload);
};
return streamFn(model, context, {
...options,

View File

@@ -1,5 +1,9 @@
import type { OAuthCredentials, OAuthProvider } from "@mariozechner/pi-ai/oauth";
import { getOAuthApiKey, getOAuthProviders } from "@mariozechner/pi-ai/oauth";
import {
getOAuthApiKey,
getOAuthProviders,
type OAuthCredentials,
type OAuthProvider,
} from "@mariozechner/pi-ai";
import { loadConfig, type OpenClawConfig } from "../../config/config.js";
import { coerceSecretRef } from "../../config/types.secrets.js";
import { withFileLock } from "../../infra/file-lock.js";

View File

@@ -233,6 +233,9 @@ export function buildKimiCodingProvider(): ProviderConfig {
cost: KIMI_CODING_DEFAULT_COST,
contextWindow: KIMI_CODING_DEFAULT_CONTEXT_WINDOW,
maxTokens: KIMI_CODING_DEFAULT_MAX_TOKENS,
compat: {
requiresOpenAiAnthropicToolPayload: true,
},
},
],
};

View File

@@ -604,7 +604,7 @@ export function createOpenAIWebSocketStreamFn(
...(prevResponseId ? { previous_response_id: prevResponseId } : {}),
...extraParams,
};
options?.onPayload?.(payload, model);
options?.onPayload?.(payload);
try {
session.manager.send(payload as Parameters<OpenAIWebSocketManager["send"]>[0]);

View File

@@ -101,7 +101,7 @@ describeGeminiLive("pi embedded extra params (gemini live)", () => {
oneByOneRedPngBase64: string;
includeImage?: boolean;
prompt: string;
onPayload?: (payload: Record<string, unknown>, model: Model<"google-generative-ai">) => void;
onPayload?: (payload: Record<string, unknown>) => void;
}): Promise<{ sawDone: boolean; stopReason?: string; errorMessage?: string }> {
const userContent: Array<
{ type: "text"; text: string } | { type: "image"; mimeType: string; data: string }
@@ -129,11 +129,8 @@ describeGeminiLive("pi embedded extra params (gemini live)", () => {
apiKey: params.apiKey,
reasoning: "high",
maxTokens: 64,
onPayload: (payload, streamModel) => {
params.onPayload?.(
payload as Record<string, unknown>,
streamModel as Model<"google-generative-ai">,
);
onPayload: (payload) => {
params.onPayload?.(payload as Record<string, unknown>);
},
},
);

View File

@@ -207,8 +207,8 @@ describe("applyExtraParamsToAgent", () => {
payload?: Record<string, unknown>;
}) {
const payload = params.payload ?? { store: false };
const baseStreamFn: StreamFn = (model, _context, options) => {
options?.onPayload?.(payload, model);
const baseStreamFn: StreamFn = (_model, _context, options) => {
options?.onPayload?.(payload);
return {} as ReturnType<StreamFn>;
};
const agent = { streamFn: baseStreamFn };
@@ -232,8 +232,8 @@ describe("applyExtraParamsToAgent", () => {
payload?: Record<string, unknown>;
}) {
const payload = params.payload ?? {};
const baseStreamFn: StreamFn = (model, _context, options) => {
options?.onPayload?.(payload, model);
const baseStreamFn: StreamFn = (_model, _context, options) => {
options?.onPayload?.(payload);
return {} as ReturnType<StreamFn>;
};
const agent = { streamFn: baseStreamFn };
@@ -276,7 +276,7 @@ describe("applyExtraParamsToAgent", () => {
const payloads: Record<string, unknown>[] = [];
const baseStreamFn: StreamFn = (_model, _context, options) => {
const payload: Record<string, unknown> = { model: "deepseek/deepseek-r1" };
options?.onPayload?.(payload, model);
options?.onPayload?.(payload);
payloads.push(payload);
return {} as ReturnType<StreamFn>;
};
@@ -308,7 +308,7 @@ describe("applyExtraParamsToAgent", () => {
const payloads: Record<string, unknown>[] = [];
const baseStreamFn: StreamFn = (_model, _context, options) => {
const payload: Record<string, unknown> = {};
options?.onPayload?.(payload, model);
options?.onPayload?.(payload);
payloads.push(payload);
return {} as ReturnType<StreamFn>;
};
@@ -332,7 +332,7 @@ describe("applyExtraParamsToAgent", () => {
const payloads: Record<string, unknown>[] = [];
const baseStreamFn: StreamFn = (_model, _context, options) => {
const payload: Record<string, unknown> = { reasoning_effort: "high" };
options?.onPayload?.(payload, model);
options?.onPayload?.(payload);
payloads.push(payload);
return {} as ReturnType<StreamFn>;
};
@@ -357,7 +357,7 @@ describe("applyExtraParamsToAgent", () => {
const payloads: Record<string, unknown>[] = [];
const baseStreamFn: StreamFn = (_model, _context, options) => {
const payload: Record<string, unknown> = { reasoning: { max_tokens: 256 } };
options?.onPayload?.(payload, model);
options?.onPayload?.(payload);
payloads.push(payload);
return {} as ReturnType<StreamFn>;
};
@@ -381,7 +381,7 @@ describe("applyExtraParamsToAgent", () => {
const payloads: Record<string, unknown>[] = [];
const baseStreamFn: StreamFn = (_model, _context, options) => {
const payload: Record<string, unknown> = { reasoning_effort: "medium" };
options?.onPayload?.(payload, model);
options?.onPayload?.(payload);
payloads.push(payload);
return {} as ReturnType<StreamFn>;
};
@@ -588,7 +588,7 @@ describe("applyExtraParamsToAgent", () => {
const payloads: Record<string, unknown>[] = [];
const baseStreamFn: StreamFn = (_model, _context, options) => {
const payload: Record<string, unknown> = { thinking: "off" };
options?.onPayload?.(payload, model);
options?.onPayload?.(payload);
payloads.push(payload);
return {} as ReturnType<StreamFn>;
};
@@ -619,7 +619,7 @@ describe("applyExtraParamsToAgent", () => {
const payloads: Record<string, unknown>[] = [];
const baseStreamFn: StreamFn = (_model, _context, options) => {
const payload: Record<string, unknown> = { thinking: "off" };
options?.onPayload?.(payload, model);
options?.onPayload?.(payload);
payloads.push(payload);
return {} as ReturnType<StreamFn>;
};
@@ -650,7 +650,7 @@ describe("applyExtraParamsToAgent", () => {
const payloads: Record<string, unknown>[] = [];
const baseStreamFn: StreamFn = (_model, _context, options) => {
const payload: Record<string, unknown> = {};
options?.onPayload?.(payload, model);
options?.onPayload?.(payload);
payloads.push(payload);
return {} as ReturnType<StreamFn>;
};
@@ -674,7 +674,7 @@ describe("applyExtraParamsToAgent", () => {
const payloads: Record<string, unknown>[] = [];
const baseStreamFn: StreamFn = (_model, _context, options) => {
const payload: Record<string, unknown> = { tool_choice: "required" };
options?.onPayload?.(payload, model);
options?.onPayload?.(payload);
payloads.push(payload);
return {} as ReturnType<StreamFn>;
};
@@ -699,7 +699,7 @@ describe("applyExtraParamsToAgent", () => {
const payloads: Record<string, unknown>[] = [];
const baseStreamFn: StreamFn = (_model, _context, options) => {
const payload: Record<string, unknown> = {};
options?.onPayload?.(payload, model);
options?.onPayload?.(payload);
payloads.push(payload);
return {} as ReturnType<StreamFn>;
};
@@ -732,7 +732,7 @@ describe("applyExtraParamsToAgent", () => {
expect(payloads[0]?.thinking).toEqual({ type: "disabled" });
});
it("does not rewrite tool schema for kimi-coding (native Anthropic format)", () => {
it("normalizes kimi-coding anthropic tools to OpenAI function format", () => {
const payloads: Record<string, unknown>[] = [];
const baseStreamFn: StreamFn = (_model, _context, options) => {
const payload: Record<string, unknown> = {
@@ -746,10 +746,18 @@ describe("applyExtraParamsToAgent", () => {
required: ["path"],
},
},
{
type: "function",
function: {
name: "exec",
description: "Run command",
parameters: { type: "object", properties: {} },
},
},
],
tool_choice: { type: "tool", name: "read" },
};
options?.onPayload?.(payload, model);
options?.onPayload?.(payload);
payloads.push(payload);
return {} as ReturnType<StreamFn>;
};
@@ -769,16 +777,68 @@ describe("applyExtraParamsToAgent", () => {
expect(payloads).toHaveLength(1);
expect(payloads[0]?.tools).toEqual([
{
name: "read",
description: "Read file",
input_schema: {
type: "object",
properties: { path: { type: "string" } },
required: ["path"],
type: "function",
function: {
name: "read",
description: "Read file",
parameters: {
type: "object",
properties: { path: { type: "string" } },
required: ["path"],
},
},
},
{
type: "function",
function: {
name: "exec",
description: "Run command",
parameters: { type: "object", properties: {} },
},
},
]);
expect(payloads[0]?.tool_choice).toEqual({ type: "tool", name: "read" });
expect(payloads[0]?.tool_choice).toEqual({
type: "function",
function: { name: "read" },
});
});
it.each([
{ input: { type: "auto" }, expected: "auto" },
{ input: { type: "none" }, expected: "none" },
{ input: { type: "required" }, expected: "required" },
])("normalizes anthropic tool_choice %j for kimi-coding endpoints", ({ input, expected }) => {
const payloads: Record<string, unknown>[] = [];
const baseStreamFn: StreamFn = (_model, _context, options) => {
const payload: Record<string, unknown> = {
tools: [
{
name: "read",
description: "Read file",
input_schema: { type: "object", properties: {} },
},
],
tool_choice: input,
};
options?.onPayload?.(payload);
payloads.push(payload);
return {} as ReturnType<StreamFn>;
};
const agent = { streamFn: baseStreamFn };
applyExtraParamsToAgent(agent, undefined, "kimi-coding", "k2p5", undefined, "low");
const model = {
api: "anthropic-messages",
provider: "kimi-coding",
id: "k2p5",
baseUrl: "https://api.kimi.com/coding/",
} as Model<"anthropic-messages">;
const context: Context = { messages: [] };
void agent.streamFn?.(model, context, {});
expect(payloads).toHaveLength(1);
expect(payloads[0]?.tool_choice).toBe(expected);
});
it("does not rewrite anthropic tool schema for non-kimi endpoints", () => {
@@ -793,7 +853,7 @@ describe("applyExtraParamsToAgent", () => {
},
],
};
options?.onPayload?.(payload, model);
options?.onPayload?.(payload);
payloads.push(payload);
return {} as ReturnType<StreamFn>;
};
@@ -832,7 +892,7 @@ describe("applyExtraParamsToAgent", () => {
},
],
};
options?.onPayload?.(payload, model);
options?.onPayload?.(payload);
payloads.push(payload);
return {} as ReturnType<StreamFn>;
};
@@ -896,7 +956,7 @@ describe("applyExtraParamsToAgent", () => {
},
},
};
options?.onPayload?.(payload, model);
options?.onPayload?.(payload);
payloads.push(payload);
return {} as ReturnType<StreamFn>;
};
@@ -943,7 +1003,7 @@ describe("applyExtraParamsToAgent", () => {
},
},
};
options?.onPayload?.(payload, model);
options?.onPayload?.(payload);
payloads.push(payload);
return {} as ReturnType<StreamFn>;
};

View File

@@ -277,7 +277,7 @@ export function createAnthropicToolPayloadCompatibilityWrapper(
const originalOnPayload = options?.onPayload;
return underlying(model, context, {
...options,
onPayload: (payload, payloadModel) => {
onPayload: (payload) => {
if (
payload &&
typeof payload === "object" &&
@@ -298,7 +298,7 @@ export function createAnthropicToolPayloadCompatibilityWrapper(
);
}
}
return originalOnPayload?.(payload, payloadModel);
originalOnPayload?.(payload);
},
});
};

View File

@@ -19,7 +19,7 @@ function applyAndCapture(params: {
const baseStreamFn: StreamFn = (_model, _context, options) => {
captured.headers = options?.headers;
options?.onPayload?.({}, model);
options?.onPayload?.({});
return createAssistantMessageEventStream();
};
const agent = { streamFn: baseStreamFn };
@@ -97,7 +97,7 @@ describe("extra-params: Kilocode kilo/auto reasoning", () => {
const baseStreamFn: StreamFn = (_model, _context, options) => {
const payload: Record<string, unknown> = { reasoning_effort: "high" };
options?.onPayload?.(payload, model);
options?.onPayload?.(payload);
capturedPayload = payload;
return createAssistantMessageEventStream();
};
@@ -125,7 +125,7 @@ describe("extra-params: Kilocode kilo/auto reasoning", () => {
const baseStreamFn: StreamFn = (_model, _context, options) => {
const payload: Record<string, unknown> = {};
options?.onPayload?.(payload, model);
options?.onPayload?.(payload);
capturedPayload = payload;
return createAssistantMessageEventStream();
};
@@ -158,7 +158,7 @@ describe("extra-params: Kilocode kilo/auto reasoning", () => {
const baseStreamFn: StreamFn = (_model, _context, options) => {
const payload: Record<string, unknown> = { reasoning_effort: "high" };
options?.onPayload?.(payload, model);
options?.onPayload?.(payload);
capturedPayload = payload;
return createAssistantMessageEventStream();
};

View File

@@ -13,7 +13,7 @@ type StreamPayload = {
function runOpenRouterPayload(payload: StreamPayload, modelId: string) {
const baseStreamFn: StreamFn = (_model, _context, options) => {
options?.onPayload?.(payload, model);
options?.onPayload?.(payload);
return createAssistantMessageEventStream();
};
const agent = { streamFn: baseStreamFn };

View File

@@ -222,7 +222,7 @@ function createGoogleThinkingPayloadWrapper(
const onPayload = options?.onPayload;
return underlying(model, context, {
...options,
onPayload: (payload, payloadModel) => {
onPayload: (payload) => {
if (model.api === "google-generative-ai") {
sanitizeGoogleThinkingPayload({
payload,
@@ -230,7 +230,7 @@ function createGoogleThinkingPayloadWrapper(
thinkingLevel,
});
}
return onPayload?.(payload, payloadModel);
onPayload?.(payload);
},
});
};
@@ -258,12 +258,12 @@ function createZaiToolStreamWrapper(
const originalOnPayload = options?.onPayload;
return underlying(model, context, {
...options,
onPayload: (payload, payloadModel) => {
onPayload: (payload) => {
if (payload && typeof payload === "object") {
// Inject tool_stream: true for Z.AI API
(payload as Record<string, unknown>).tool_stream = true;
}
return originalOnPayload?.(payload, payloadModel);
originalOnPayload?.(payload);
},
});
};
@@ -306,11 +306,11 @@ function createParallelToolCallsWrapper(
const originalOnPayload = options?.onPayload;
return underlying(model, context, {
...options,
onPayload: (payload, payloadModel) => {
onPayload: (payload) => {
if (payload && typeof payload === "object") {
(payload as Record<string, unknown>).parallel_tool_calls = enabled;
}
return originalOnPayload?.(payload, payloadModel);
originalOnPayload?.(payload);
},
});
};

View File

@@ -21,8 +21,8 @@ type ToolStreamCase = {
function runToolStreamCase(params: ToolStreamCase) {
const payload: Record<string, unknown> = { model: params.model.id, messages: [] };
const baseStreamFn: StreamFn = (model, _context, options) => {
options?.onPayload?.(payload, model);
const baseStreamFn: StreamFn = (_model, _context, options) => {
options?.onPayload?.(payload);
return {} as ReturnType<StreamFn>;
};
const agent = { streamFn: baseStreamFn };

View File

@@ -53,14 +53,14 @@ export function createSiliconFlowThinkingWrapper(baseStreamFn: StreamFn | undefi
const originalOnPayload = options?.onPayload;
return underlying(model, context, {
...options,
onPayload: (payload, payloadModel) => {
onPayload: (payload) => {
if (payload && typeof payload === "object") {
const payloadObj = payload as Record<string, unknown>;
if (payloadObj.thinking === "off") {
payloadObj.thinking = null;
}
}
return originalOnPayload?.(payload, payloadModel);
originalOnPayload?.(payload);
},
});
};
@@ -89,7 +89,7 @@ export function createMoonshotThinkingWrapper(
const originalOnPayload = options?.onPayload;
return underlying(model, context, {
...options,
onPayload: (payload, payloadModel) => {
onPayload: (payload) => {
if (payload && typeof payload === "object") {
const payloadObj = payload as Record<string, unknown>;
let effectiveThinkingType = normalizeMoonshotThinkingType(payloadObj.thinking);
@@ -106,7 +106,7 @@ export function createMoonshotThinkingWrapper(
payloadObj.tool_choice = "auto";
}
}
return originalOnPayload?.(payload, payloadModel);
originalOnPayload?.(payload);
},
});
};

View File

@@ -187,7 +187,7 @@ export function createOpenAIResponsesContextManagementWrapper(
const originalOnPayload = options?.onPayload;
return underlying(model, context, {
...options,
onPayload: (payload, payloadModel) => {
onPayload: (payload) => {
if (payload && typeof payload === "object") {
applyOpenAIResponsesPayloadOverrides({
payloadObj: payload as Record<string, unknown>,
@@ -197,7 +197,7 @@ export function createOpenAIResponsesContextManagementWrapper(
compactThreshold,
});
}
return originalOnPayload?.(payload, payloadModel);
originalOnPayload?.(payload);
},
});
};
@@ -219,14 +219,14 @@ export function createOpenAIServiceTierWrapper(
const originalOnPayload = options?.onPayload;
return underlying(model, context, {
...options,
onPayload: (payload, payloadModel) => {
onPayload: (payload) => {
if (payload && typeof payload === "object") {
const payloadObj = payload as Record<string, unknown>;
if (payloadObj.service_tier === undefined) {
payloadObj.service_tier = serviceTier;
}
}
return originalOnPayload?.(payload, payloadModel);
originalOnPayload?.(payload);
},
});
};

View File

@@ -73,7 +73,7 @@ export function createOpenRouterSystemCacheWrapper(baseStreamFn: StreamFn | unde
const originalOnPayload = options?.onPayload;
return underlying(model, context, {
...options,
onPayload: (payload, payloadModel) => {
onPayload: (payload) => {
const messages = (payload as Record<string, unknown>)?.messages;
if (Array.isArray(messages)) {
for (const msg of messages as Array<{ role?: string; content?: unknown }>) {
@@ -92,7 +92,7 @@ export function createOpenRouterSystemCacheWrapper(baseStreamFn: StreamFn | unde
}
}
}
return originalOnPayload?.(payload, payloadModel);
originalOnPayload?.(payload);
},
});
};
@@ -111,9 +111,9 @@ export function createOpenRouterWrapper(
...OPENROUTER_APP_HEADERS,
...options?.headers,
},
onPayload: (payload, payloadModel) => {
onPayload: (payload) => {
normalizeProxyReasoningPayload(payload, thinkingLevel);
return onPayload?.(payload, payloadModel);
onPayload?.(payload);
},
});
};
@@ -136,9 +136,9 @@ export function createKilocodeWrapper(
...options?.headers,
...resolveKilocodeAppHeaders(),
},
onPayload: (payload, payloadModel) => {
onPayload: (payload) => {
normalizeProxyReasoningPayload(payload, thinkingLevel);
return onPayload?.(payload, payloadModel);
onPayload?.(payload);
},
});
};

View File

@@ -520,7 +520,7 @@ describe("wrapOllamaCompatNumCtx", () => {
let payloadSeen: Record<string, unknown> | undefined;
const baseFn = vi.fn((_model, _context, options) => {
const payload: Record<string, unknown> = { options: { temperature: 0.1 } };
options?.onPayload?.(payload, _model);
options?.onPayload?.(payload);
payloadSeen = payload;
return {} as never;
});

View File

@@ -124,7 +124,6 @@ import { installToolResultContextGuard } from "../tool-result-context-guard.js";
import { splitSdkTools } from "../tool-split.js";
import { describeUnknownError, mapThinkingLevel } from "../utils.js";
import { flushPendingToolResultsAfterIdle } from "../wait-for-idle-before-flush.js";
import { waitForCompactionRetryWithAggregateTimeout } from "./compaction-retry-aggregate-timeout.js";
import {
selectCompactionTimeoutSnapshot,
shouldFlagCompactionTimeout,
@@ -228,16 +227,17 @@ export function wrapOllamaCompatNumCtx(baseFn: StreamFn | undefined, numCtx: num
return (model, context, options) =>
streamFn(model, context, {
...options,
onPayload: (payload: unknown, payloadModel) => {
onPayload: (payload: unknown) => {
if (!payload || typeof payload !== "object") {
return options?.onPayload?.(payload, payloadModel);
options?.onPayload?.(payload);
return;
}
const payloadRecord = payload as Record<string, unknown>;
if (!payloadRecord.options || typeof payloadRecord.options !== "object") {
payloadRecord.options = {};
}
(payloadRecord.options as Record<string, unknown>).num_ctx = numCtx;
return options?.onPayload?.(payload, payloadModel);
options?.onPayload?.(payload);
},
});
}
@@ -1538,7 +1538,6 @@ export async function runEmbeddedAttempt(
toolMetas,
unsubscribe,
waitForCompactionRetry,
isCompactionInFlight,
getMessagingToolSentTexts,
getMessagingToolSentMediaUrls,
getMessagingToolSentTargets,
@@ -1800,7 +1799,6 @@ export async function runEmbeddedAttempt(
// Only trust snapshot if compaction wasn't running before or after capture
const preCompactionSnapshot = wasCompactingBefore || wasCompactingAfter ? null : snapshot;
const preCompactionSessionId = activeSession.sessionId;
const COMPACTION_RETRY_AGGREGATE_TIMEOUT_MS = 60_000;
try {
// Flush buffered block replies before waiting for compaction so the
@@ -1811,21 +1809,7 @@ export async function runEmbeddedAttempt(
await params.onBlockReplyFlush();
}
const compactionRetryWait = await waitForCompactionRetryWithAggregateTimeout({
waitForCompactionRetry,
abortable,
aggregateTimeoutMs: COMPACTION_RETRY_AGGREGATE_TIMEOUT_MS,
isCompactionStillInFlight: isCompactionInFlight,
});
if (compactionRetryWait.timedOut) {
timedOutDuringCompaction = true;
if (!isProbeSession) {
log.warn(
`compaction retry aggregate timeout (${COMPACTION_RETRY_AGGREGATE_TIMEOUT_MS}ms): ` +
`proceeding with pre-compaction state runId=${params.runId} sessionId=${params.sessionId}`,
);
}
}
await abortable(waitForCompactionRetry());
} catch (err) {
if (isRunnerAbortError(err)) {
if (!promptError) {

View File

@@ -1,143 +0,0 @@
import { describe, expect, it, vi } from "vitest";
import { waitForCompactionRetryWithAggregateTimeout } from "./compaction-retry-aggregate-timeout.js";
describe("waitForCompactionRetryWithAggregateTimeout", () => {
it("times out and fires callback when compaction retry never resolves", async () => {
vi.useFakeTimers();
try {
const onTimeout = vi.fn();
const waitForCompactionRetry = vi.fn(async () => await new Promise<void>(() => {}));
const resultPromise = waitForCompactionRetryWithAggregateTimeout({
waitForCompactionRetry,
abortable: async (promise) => await promise,
aggregateTimeoutMs: 60_000,
onTimeout,
});
await vi.advanceTimersByTimeAsync(60_000);
const result = await resultPromise;
expect(result.timedOut).toBe(true);
expect(onTimeout).toHaveBeenCalledTimes(1);
expect(vi.getTimerCount()).toBe(0);
} finally {
await vi.runOnlyPendingTimersAsync();
vi.useRealTimers();
}
});
it("keeps waiting while compaction remains in flight", async () => {
vi.useFakeTimers();
try {
const onTimeout = vi.fn();
let compactionInFlight = true;
const waitForCompactionRetry = vi.fn(
async () =>
await new Promise<void>((resolve) => {
setTimeout(() => {
compactionInFlight = false;
resolve();
}, 170_000);
}),
);
const resultPromise = waitForCompactionRetryWithAggregateTimeout({
waitForCompactionRetry,
abortable: async (promise) => await promise,
aggregateTimeoutMs: 60_000,
onTimeout,
isCompactionStillInFlight: () => compactionInFlight,
});
await vi.advanceTimersByTimeAsync(170_000);
const result = await resultPromise;
expect(result.timedOut).toBe(false);
expect(onTimeout).not.toHaveBeenCalled();
expect(vi.getTimerCount()).toBe(0);
} finally {
await vi.runOnlyPendingTimersAsync();
vi.useRealTimers();
}
});
it("times out after an idle timeout window", async () => {
vi.useFakeTimers();
try {
const onTimeout = vi.fn();
let compactionInFlight = true;
const waitForCompactionRetry = vi.fn(async () => await new Promise<void>(() => {}));
setTimeout(() => {
compactionInFlight = false;
}, 90_000);
const resultPromise = waitForCompactionRetryWithAggregateTimeout({
waitForCompactionRetry,
abortable: async (promise) => await promise,
aggregateTimeoutMs: 60_000,
onTimeout,
isCompactionStillInFlight: () => compactionInFlight,
});
await vi.advanceTimersByTimeAsync(120_000);
const result = await resultPromise;
expect(result.timedOut).toBe(true);
expect(onTimeout).toHaveBeenCalledTimes(1);
expect(vi.getTimerCount()).toBe(0);
} finally {
await vi.runOnlyPendingTimersAsync();
vi.useRealTimers();
}
});
it("does not time out when compaction retry resolves", async () => {
vi.useFakeTimers();
try {
const onTimeout = vi.fn();
const waitForCompactionRetry = vi.fn(async () => {});
const result = await waitForCompactionRetryWithAggregateTimeout({
waitForCompactionRetry,
abortable: async (promise) => await promise,
aggregateTimeoutMs: 60_000,
onTimeout,
});
expect(result.timedOut).toBe(false);
expect(onTimeout).not.toHaveBeenCalled();
expect(vi.getTimerCount()).toBe(0);
} finally {
await vi.runOnlyPendingTimersAsync();
vi.useRealTimers();
}
});
it("propagates abort errors from abortable and clears timer", async () => {
vi.useFakeTimers();
try {
const abortError = new Error("aborted");
abortError.name = "AbortError";
const onTimeout = vi.fn();
const waitForCompactionRetry = vi.fn(async () => await new Promise<void>(() => {}));
await expect(
waitForCompactionRetryWithAggregateTimeout({
waitForCompactionRetry,
abortable: async () => {
throw abortError;
},
aggregateTimeoutMs: 60_000,
onTimeout,
}),
).rejects.toThrow("aborted");
expect(onTimeout).not.toHaveBeenCalled();
expect(vi.getTimerCount()).toBe(0);
} finally {
await vi.runOnlyPendingTimersAsync();
vi.useRealTimers();
}
});
});

View File

@@ -1,51 +0,0 @@
/**
* Wait for compaction retry completion with an aggregate timeout to avoid
* holding a session lane indefinitely when retry resolution is lost.
*/
export async function waitForCompactionRetryWithAggregateTimeout(params: {
waitForCompactionRetry: () => Promise<void>;
abortable: <T>(promise: Promise<T>) => Promise<T>;
aggregateTimeoutMs: number;
onTimeout?: () => void;
isCompactionStillInFlight?: () => boolean;
}): Promise<{ timedOut: boolean }> {
const timeoutMsRaw = params.aggregateTimeoutMs;
const timeoutMs = Number.isFinite(timeoutMsRaw) ? Math.max(1, Math.floor(timeoutMsRaw)) : 1;
let timedOut = false;
const waitPromise = params.waitForCompactionRetry().then(() => "done" as const);
while (true) {
let timer: ReturnType<typeof setTimeout> | undefined;
try {
const result = await params.abortable(
Promise.race([
waitPromise,
new Promise<"timeout">((resolve) => {
timer = setTimeout(() => resolve("timeout"), timeoutMs);
}),
]),
);
if (result === "done") {
break;
}
// Keep extending the timeout window while compaction is actively running.
// We only trigger the fallback timeout once compaction appears idle.
if (params.isCompactionStillInFlight?.()) {
continue;
}
timedOut = true;
params.onTimeout?.();
break;
} finally {
if (timer !== undefined) {
clearTimeout(timer);
}
}
}
return { timedOut };
}

View File

@@ -1,108 +0,0 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import {
__testing,
abortEmbeddedPiRun,
clearActiveEmbeddedRun,
setActiveEmbeddedRun,
waitForActiveEmbeddedRuns,
} from "./runs.js";
describe("pi-embedded runner run registry", () => {
afterEach(() => {
__testing.resetActiveEmbeddedRuns();
vi.restoreAllMocks();
});
it("aborts only compacting runs in compacting mode", () => {
const abortCompacting = vi.fn();
const abortNormal = vi.fn();
setActiveEmbeddedRun("session-compacting", {
queueMessage: async () => {},
isStreaming: () => true,
isCompacting: () => true,
abort: abortCompacting,
});
setActiveEmbeddedRun("session-normal", {
queueMessage: async () => {},
isStreaming: () => true,
isCompacting: () => false,
abort: abortNormal,
});
const aborted = abortEmbeddedPiRun(undefined, { mode: "compacting" });
expect(aborted).toBe(true);
expect(abortCompacting).toHaveBeenCalledTimes(1);
expect(abortNormal).not.toHaveBeenCalled();
});
it("aborts every active run in all mode", () => {
const abortA = vi.fn();
const abortB = vi.fn();
setActiveEmbeddedRun("session-a", {
queueMessage: async () => {},
isStreaming: () => true,
isCompacting: () => true,
abort: abortA,
});
setActiveEmbeddedRun("session-b", {
queueMessage: async () => {},
isStreaming: () => true,
isCompacting: () => false,
abort: abortB,
});
const aborted = abortEmbeddedPiRun(undefined, { mode: "all" });
expect(aborted).toBe(true);
expect(abortA).toHaveBeenCalledTimes(1);
expect(abortB).toHaveBeenCalledTimes(1);
});
it("waits for active runs to drain", async () => {
vi.useFakeTimers();
try {
const handle = {
queueMessage: async () => {},
isStreaming: () => true,
isCompacting: () => false,
abort: vi.fn(),
};
setActiveEmbeddedRun("session-a", handle);
setTimeout(() => {
clearActiveEmbeddedRun("session-a", handle);
}, 500);
const waitPromise = waitForActiveEmbeddedRuns(1_000, { pollMs: 100 });
await vi.advanceTimersByTimeAsync(500);
const result = await waitPromise;
expect(result.drained).toBe(true);
} finally {
await vi.runOnlyPendingTimersAsync();
vi.useRealTimers();
}
});
it("returns drained=false when timeout elapses", async () => {
vi.useFakeTimers();
try {
setActiveEmbeddedRun("session-a", {
queueMessage: async () => {},
isStreaming: () => true,
isCompacting: () => false,
abort: vi.fn(),
});
const waitPromise = waitForActiveEmbeddedRuns(1_000, { pollMs: 100 });
await vi.advanceTimersByTimeAsync(1_000);
const result = await waitPromise;
expect(result.drained).toBe(false);
} finally {
await vi.runOnlyPendingTimersAsync();
vi.useRealTimers();
}
});
});

View File

@@ -37,70 +37,15 @@ export function queueEmbeddedPiMessage(sessionId: string, text: string): boolean
return true;
}
/**
* Abort embedded PI runs.
*
* - With a sessionId, aborts that single run.
* - With no sessionId, supports targeted abort modes (for example, compacting runs only).
*/
export function abortEmbeddedPiRun(sessionId: string): boolean;
export function abortEmbeddedPiRun(
sessionId: undefined,
opts: { mode: "all" | "compacting" },
): boolean;
export function abortEmbeddedPiRun(
sessionId?: string,
opts?: { mode?: "all" | "compacting" },
): boolean {
if (typeof sessionId === "string" && sessionId.length > 0) {
const handle = ACTIVE_EMBEDDED_RUNS.get(sessionId);
if (!handle) {
diag.debug(`abort failed: sessionId=${sessionId} reason=no_active_run`);
return false;
}
diag.debug(`aborting run: sessionId=${sessionId}`);
try {
handle.abort();
} catch (err) {
diag.warn(`abort failed: sessionId=${sessionId} err=${String(err)}`);
return false;
}
return true;
export function abortEmbeddedPiRun(sessionId: string): boolean {
const handle = ACTIVE_EMBEDDED_RUNS.get(sessionId);
if (!handle) {
diag.debug(`abort failed: sessionId=${sessionId} reason=no_active_run`);
return false;
}
const mode = opts?.mode;
if (mode === "compacting") {
let aborted = false;
for (const [id, handle] of ACTIVE_EMBEDDED_RUNS) {
if (!handle.isCompacting()) {
continue;
}
diag.debug(`aborting compacting run: sessionId=${id}`);
try {
handle.abort();
aborted = true;
} catch (err) {
diag.warn(`abort failed: sessionId=${id} err=${String(err)}`);
}
}
return aborted;
}
if (mode === "all") {
let aborted = false;
for (const [id, handle] of ACTIVE_EMBEDDED_RUNS) {
diag.debug(`aborting run: sessionId=${id}`);
try {
handle.abort();
aborted = true;
} catch (err) {
diag.warn(`abort failed: sessionId=${id} err=${String(err)}`);
}
}
return aborted;
}
return false;
diag.debug(`aborting run: sessionId=${sessionId}`);
handle.abort();
return true;
}
export function isEmbeddedPiRunActive(sessionId: string): boolean {
@@ -123,36 +68,6 @@ export function getActiveEmbeddedRunCount(): number {
return ACTIVE_EMBEDDED_RUNS.size;
}
/**
* Wait for active embedded runs to drain.
*
* Used during restarts so in-flight compaction runs can release session write
* locks before the next lifecycle starts.
*/
export async function waitForActiveEmbeddedRuns(
timeoutMs = 15_000,
opts?: { pollMs?: number },
): Promise<{ drained: boolean }> {
const pollMsRaw = opts?.pollMs ?? 250;
const pollMs = Math.max(10, Math.floor(pollMsRaw));
const maxWaitMs = Math.max(pollMs, Math.floor(timeoutMs));
const startedAt = Date.now();
while (true) {
if (ACTIVE_EMBEDDED_RUNS.size === 0) {
return { drained: true };
}
const elapsedMs = Date.now() - startedAt;
if (elapsedMs >= maxWaitMs) {
diag.warn(
`wait for active embedded runs timed out: activeRuns=${ACTIVE_EMBEDDED_RUNS.size} timeoutMs=${maxWaitMs}`,
);
return { drained: false };
}
await new Promise<void>((resolve) => setTimeout(resolve, pollMs));
}
}
export function waitForEmbeddedPiRunEnd(sessionId: string, timeoutMs = 15_000): Promise<boolean> {
if (!sessionId || !ACTIVE_EMBEDDED_RUNS.has(sessionId)) {
return Promise.resolve(true);
@@ -235,17 +150,4 @@ export function clearActiveEmbeddedRun(
}
}
export const __testing = {
resetActiveEmbeddedRuns() {
for (const waiters of EMBEDDED_RUN_WAITERS.values()) {
for (const waiter of waiters) {
clearTimeout(waiter.timer);
waiter.resolve(true);
}
}
EMBEDDED_RUN_WAITERS.clear();
ACTIVE_EMBEDDED_RUNS.clear();
},
};
export type { EmbeddedPiQueueHandle };

View File

@@ -187,7 +187,7 @@ describe("resolveEffectiveToolPolicy", () => {
},
} as OpenClawConfig;
const result = resolveEffectiveToolPolicy({ config: cfg });
expect(result.profileAlsoAllow).toEqual(["exec", "process"]);
expect(result.profiles.primary.alsoAllow).toEqual(["exec", "process"]);
});
it("implicitly re-exposes read, write, and edit when tools.fs is configured", () => {
@@ -198,7 +198,7 @@ describe("resolveEffectiveToolPolicy", () => {
},
} as OpenClawConfig;
const result = resolveEffectiveToolPolicy({ config: cfg });
expect(result.profileAlsoAllow).toEqual(["read", "write", "edit"]);
expect(result.profiles.primary.alsoAllow).toEqual(["read", "write", "edit"]);
});
it("merges explicit alsoAllow with implicit tool-section exposure", () => {
@@ -210,7 +210,7 @@ describe("resolveEffectiveToolPolicy", () => {
},
} as OpenClawConfig;
const result = resolveEffectiveToolPolicy({ config: cfg });
expect(result.profileAlsoAllow).toEqual(["web_search", "exec", "process"]);
expect(result.profiles.primary.alsoAllow).toEqual(["web_search", "exec", "process"]);
});
it("uses agent tool sections when resolving implicit exposure", () => {
@@ -230,6 +230,6 @@ describe("resolveEffectiveToolPolicy", () => {
},
} as OpenClawConfig;
const result = resolveEffectiveToolPolicy({ config: cfg, agentId: "coder" });
expect(result.profileAlsoAllow).toEqual(["read", "write", "edit"]);
expect(result.profiles.primary.alsoAllow).toEqual(["read", "write", "edit"]);
});
});

View File

@@ -2,7 +2,7 @@ import { getChannelDock } from "../channels/dock.js";
import { DEFAULT_SUBAGENT_MAX_SPAWN_DEPTH } from "../config/agent-limits.js";
import type { OpenClawConfig } from "../config/config.js";
import { resolveChannelGroupToolsPolicy } from "../config/group-policy.js";
import type { AgentToolsConfig } from "../config/types.tools.js";
import type { ToolPolicyConfig } from "../config/types.tools.js";
import { normalizeAgentId } from "../routing/session-key.js";
import { resolveThreadParentSessionKey } from "../sessions/session-key-utils.js";
import { normalizeMessageChannel } from "../utils/message-channel.js";
@@ -11,6 +11,11 @@ import { compileGlobPatterns, matchesAnyGlobPattern } from "./glob-pattern.js";
import type { AnyAgentTool } from "./pi-tools.types.js";
import { pickSandboxToolPolicy } from "./sandbox-tool-policy.js";
import type { SandboxToolPolicy } from "./sandbox.js";
import {
mergeAlsoAllowIntoAllowlist,
resolveProfileAlsoAllow,
resolveProviderProfileAlsoAllow,
} from "./tool-policy-also-allow.js";
import { expandToolGroups, normalizeToolName } from "./tool-policy.js";
function makeToolPolicyMatcher(policy: SandboxToolPolicy) {
@@ -99,7 +104,7 @@ export function resolveSubagentToolPolicy(cfg?: OpenClawConfig, depth?: number):
...baseDeny.filter((toolName) => !explicitAllow.has(normalizeToolName(toolName))),
...(Array.isArray(configured?.deny) ? configured.deny : []),
];
const mergedAllow = allow && alsoAllow ? Array.from(new Set([...allow, ...alsoAllow])) : allow;
const mergedAllow = mergeAlsoAllowIntoAllowlist({ allow, alsoAllow });
return { allow: mergedAllow, deny };
}
@@ -118,11 +123,9 @@ export function filterToolsByPolicy(tools: AnyAgentTool[], policy?: SandboxToolP
return tools.filter((tool) => matcher(tool.name));
}
type ToolPolicyConfig = {
allow?: string[];
export type ResolvedToolProfileScope = {
id?: string;
alsoAllow?: string[];
deny?: string[];
profile?: string;
};
function normalizeProviderKey(value: string): string {
@@ -197,36 +200,19 @@ function resolveProviderToolPolicy(params: {
return undefined;
}
function resolveExplicitProfileAlsoAllow(tools?: OpenClawConfig["tools"]): string[] | undefined {
return Array.isArray(tools?.alsoAllow) ? tools.alsoAllow : undefined;
}
function hasExplicitToolSection(section: unknown): boolean {
return section !== undefined && section !== null;
}
function resolveImplicitProfileAlsoAllow(params: {
globalTools?: OpenClawConfig["tools"];
agentTools?: AgentToolsConfig;
}): string[] | undefined {
const implicit = new Set<string>();
if (
hasExplicitToolSection(params.agentTools?.exec) ||
hasExplicitToolSection(params.globalTools?.exec)
) {
implicit.add("exec");
implicit.add("process");
}
if (
hasExplicitToolSection(params.agentTools?.fs) ||
hasExplicitToolSection(params.globalTools?.fs)
) {
implicit.add("read");
implicit.add("write");
implicit.add("edit");
}
return implicit.size > 0 ? Array.from(implicit) : undefined;
}
export type ResolvedToolPolicyContext = {
agentId?: string;
profiles: {
primary: ResolvedToolProfileScope;
provider: ResolvedToolProfileScope;
};
sandboxPolicies: {
global?: SandboxToolPolicy;
globalProvider?: SandboxToolPolicy;
agent?: SandboxToolPolicy;
agentProvider?: SandboxToolPolicy;
};
};
export function resolveEffectiveToolPolicy(params: {
config?: OpenClawConfig;
@@ -234,7 +220,7 @@ export function resolveEffectiveToolPolicy(params: {
agentId?: string;
modelProvider?: string;
modelId?: string;
}) {
}): ResolvedToolPolicyContext {
const explicitAgentId =
typeof params.agentId === "string" && params.agentId.trim()
? normalizeAgentId(params.agentId)
@@ -258,30 +244,27 @@ export function resolveEffectiveToolPolicy(params: {
modelProvider: params.modelProvider,
modelId: params.modelId,
});
const explicitProfileAlsoAllow =
resolveExplicitProfileAlsoAllow(agentTools) ?? resolveExplicitProfileAlsoAllow(globalTools);
const implicitProfileAlsoAllow = resolveImplicitProfileAlsoAllow({ globalTools, agentTools });
const profileAlsoAllow =
explicitProfileAlsoAllow || implicitProfileAlsoAllow
? Array.from(
new Set([...(explicitProfileAlsoAllow ?? []), ...(implicitProfileAlsoAllow ?? [])]),
)
: undefined;
return {
agentId,
globalPolicy: pickSandboxToolPolicy(globalTools),
globalProviderPolicy: pickSandboxToolPolicy(providerPolicy),
agentPolicy: pickSandboxToolPolicy(agentTools),
agentProviderPolicy: pickSandboxToolPolicy(agentProviderPolicy),
profile,
providerProfile: agentProviderPolicy?.profile ?? providerPolicy?.profile,
// alsoAllow is applied at the profile stage (to avoid being filtered out early).
profileAlsoAllow,
providerProfileAlsoAllow: Array.isArray(agentProviderPolicy?.alsoAllow)
? agentProviderPolicy?.alsoAllow
: Array.isArray(providerPolicy?.alsoAllow)
? providerPolicy?.alsoAllow
: undefined,
profiles: {
primary: {
id: profile,
alsoAllow: resolveProfileAlsoAllow({ globalTools, agentTools }),
},
provider: {
id: agentProviderPolicy?.profile ?? providerPolicy?.profile,
alsoAllow: resolveProviderProfileAlsoAllow({
providerPolicy,
agentProviderPolicy,
}),
},
},
sandboxPolicies: {
global: pickSandboxToolPolicy(globalTools),
globalProvider: pickSandboxToolPolicy(providerPolicy),
agent: pickSandboxToolPolicy(agentTools),
agentProvider: pickSandboxToolPolicy(agentProviderPolicy),
},
};
}

View File

@@ -1,6 +1,10 @@
import { codingTools, createReadTool, readTool } from "@mariozechner/pi-coding-agent";
import type { OpenClawConfig } from "../config/config.js";
import type { ToolLoopDetectionConfig } from "../config/types.tools.js";
import type {
AgentToolsConfig,
ToolLoopDetectionConfig,
ToolsConfig,
} from "../config/types.tools.js";
import { resolveMergedSafeBinProfileFixtures } from "../infra/exec-safe-bin-runtime-policy.js";
import { logWarn } from "../logger.js";
import { getPluginToolMeta } from "../plugins/tools.js";
@@ -129,32 +133,33 @@ function isApplyPatchAllowedForModel(params: {
});
}
function resolveExecConfig(params: { cfg?: OpenClawConfig; agentId?: string }) {
function resolveToolSectionConfig<T>(
params: { cfg?: OpenClawConfig; agentId?: string },
select: (tools: ToolsConfig | AgentToolsConfig | undefined) => T | undefined,
): {
global?: T;
agent?: T;
} {
const cfg = params.cfg;
const globalExec = cfg?.tools?.exec;
const agentExec =
cfg && params.agentId ? resolveAgentConfig(cfg, params.agentId)?.tools?.exec : undefined;
return {
host: agentExec?.host ?? globalExec?.host,
security: agentExec?.security ?? globalExec?.security,
ask: agentExec?.ask ?? globalExec?.ask,
node: agentExec?.node ?? globalExec?.node,
pathPrepend: agentExec?.pathPrepend ?? globalExec?.pathPrepend,
safeBins: agentExec?.safeBins ?? globalExec?.safeBins,
safeBinTrustedDirs: agentExec?.safeBinTrustedDirs ?? globalExec?.safeBinTrustedDirs,
global: select(cfg?.tools),
agent:
cfg && params.agentId ? select(resolveAgentConfig(cfg, params.agentId)?.tools) : undefined,
};
}
function resolveExecConfig(params: { cfg?: OpenClawConfig; agentId?: string }) {
const { global: globalExec, agent: agentExec } = resolveToolSectionConfig(
params,
(tools) => tools?.exec,
);
return {
...globalExec,
...agentExec,
safeBinProfiles: resolveMergedSafeBinProfileFixtures({
global: globalExec,
local: agentExec,
}),
backgroundMs: agentExec?.backgroundMs ?? globalExec?.backgroundMs,
timeoutSec: agentExec?.timeoutSec ?? globalExec?.timeoutSec,
approvalRunningNoticeMs:
agentExec?.approvalRunningNoticeMs ?? globalExec?.approvalRunningNoticeMs,
cleanupMs: agentExec?.cleanupMs ?? globalExec?.cleanupMs,
notifyOnExit: agentExec?.notifyOnExit ?? globalExec?.notifyOnExit,
notifyOnExitEmptySuccess:
agentExec?.notifyOnExitEmptySuccess ?? globalExec?.notifyOnExitEmptySuccess,
applyPatch: agentExec?.applyPatch ?? globalExec?.applyPatch,
};
}
@@ -162,11 +167,7 @@ export function resolveToolLoopDetectionConfig(params: {
cfg?: OpenClawConfig;
agentId?: string;
}): ToolLoopDetectionConfig | undefined {
const global = params.cfg?.tools?.loopDetection;
const agent =
params.agentId && params.cfg
? resolveAgentConfig(params.cfg, params.agentId)?.tools?.loopDetection
: undefined;
const { global, agent } = resolveToolSectionConfig(params, (tools) => tools?.loopDetection);
if (!agent) {
return global;
@@ -258,23 +259,14 @@ export function createOpenClawCodingTools(options?: {
}): AnyAgentTool[] {
const execToolName = "exec";
const sandbox = options?.sandbox?.enabled ? options.sandbox : undefined;
const {
agentId,
globalPolicy,
globalProviderPolicy,
agentPolicy,
agentProviderPolicy,
profile,
providerProfile,
profileAlsoAllow,
providerProfileAlsoAllow,
} = resolveEffectiveToolPolicy({
const policyContext = resolveEffectiveToolPolicy({
config: options?.config,
sessionKey: options?.sessionKey,
agentId: options?.agentId,
modelProvider: options?.modelProvider,
modelId: options?.modelId,
});
const { agentId } = policyContext;
const groupPolicy = resolveGroupToolPolicy({
config: options?.config,
sessionKey: options?.sessionKey,
@@ -289,13 +281,13 @@ export function createOpenClawCodingTools(options?: {
senderUsername: options?.senderUsername,
senderE164: options?.senderE164,
});
const profilePolicy = resolveToolProfilePolicy(profile);
const providerProfilePolicy = resolveToolProfilePolicy(providerProfile);
const profilePolicyWithAlsoAllow = mergeAlsoAllowPolicy(profilePolicy, profileAlsoAllow);
const profilePolicyWithAlsoAllow = mergeAlsoAllowPolicy(
resolveToolProfilePolicy(policyContext.profiles.primary.id),
policyContext.profiles.primary.alsoAllow,
);
const providerProfilePolicyWithAlsoAllow = mergeAlsoAllowPolicy(
providerProfilePolicy,
providerProfileAlsoAllow,
resolveToolProfilePolicy(policyContext.profiles.provider.id),
policyContext.profiles.provider.alsoAllow,
);
// Prefer sessionKey for process isolation scope to prevent cross-session process visibility/killing.
// Fallback to agentId if no sessionKey is available (e.g. legacy or global contexts).
@@ -311,10 +303,10 @@ export function createOpenClawCodingTools(options?: {
const allowBackground = isToolAllowedByPolicies("process", [
profilePolicyWithAlsoAllow,
providerProfilePolicyWithAlsoAllow,
globalPolicy,
globalProviderPolicy,
agentPolicy,
agentProviderPolicy,
policyContext.sandboxPolicies.global,
policyContext.sandboxPolicies.globalProvider,
policyContext.sandboxPolicies.agent,
policyContext.sandboxPolicies.agentProvider,
groupPolicy,
sandbox?.tools,
subagentPolicy,
@@ -491,12 +483,12 @@ export function createOpenClawCodingTools(options?: {
sandboxed: !!sandbox,
config: options?.config,
pluginToolAllowlist: collectExplicitAllowlist([
profilePolicy,
providerProfilePolicy,
globalPolicy,
globalProviderPolicy,
agentPolicy,
agentProviderPolicy,
profilePolicyWithAlsoAllow,
providerProfilePolicyWithAlsoAllow,
policyContext.sandboxPolicies.global,
policyContext.sandboxPolicies.globalProvider,
policyContext.sandboxPolicies.agent,
policyContext.sandboxPolicies.agentProvider,
groupPolicy,
sandbox?.tools,
subagentPolicy,
@@ -530,13 +522,13 @@ export function createOpenClawCodingTools(options?: {
steps: [
...buildDefaultToolPolicyPipelineSteps({
profilePolicy: profilePolicyWithAlsoAllow,
profile,
profile: policyContext.profiles.primary.id,
providerProfilePolicy: providerProfilePolicyWithAlsoAllow,
providerProfile,
globalPolicy,
globalProviderPolicy,
agentPolicy,
agentProviderPolicy,
providerProfile: policyContext.profiles.provider.id,
globalPolicy: policyContext.sandboxPolicies.global,
globalProviderPolicy: policyContext.sandboxPolicies.globalProvider,
agentPolicy: policyContext.sandboxPolicies.agent,
agentProviderPolicy: policyContext.sandboxPolicies.agentProvider,
groupPolicy,
agentId,
}),

View File

@@ -31,8 +31,8 @@ describe("resolveProviderCapabilities", () => {
resolveProviderCapabilities("kimi-code"),
);
expect(resolveProviderCapabilities("kimi-code")).toEqual({
anthropicToolSchemaMode: "native",
anthropicToolChoiceMode: "native",
anthropicToolSchemaMode: "openai-functions",
anthropicToolChoiceMode: "openai-string-modes",
providerFamily: "default",
preserveAnthropicThinkingSignatures: false,
openAiCompatTurnValidation: true,
@@ -66,9 +66,9 @@ describe("resolveProviderCapabilities", () => {
expect(resolveTranscriptToolCallIdMode("mistral", "mistral-large-latest")).toBe("strict9");
});
it("treats kimi aliases as native anthropic tool payload providers", () => {
expect(requiresOpenAiCompatibleAnthropicToolPayload("kimi-coding")).toBe(false);
expect(requiresOpenAiCompatibleAnthropicToolPayload("kimi-code")).toBe(false);
it("treats kimi aliases as anthropic tool payload compatibility providers", () => {
expect(requiresOpenAiCompatibleAnthropicToolPayload("kimi-coding")).toBe(true);
expect(requiresOpenAiCompatibleAnthropicToolPayload("kimi-code")).toBe(true);
expect(requiresOpenAiCompatibleAnthropicToolPayload("anthropic")).toBe(false);
});

View File

@@ -33,9 +33,9 @@ const PROVIDER_CAPABILITIES: Record<string, Partial<ProviderCapabilities>> = {
"amazon-bedrock": {
providerFamily: "anthropic",
},
// kimi-coding natively supports Anthropic tool framing (input_schema);
// converting to OpenAI format causes XML text fallback instead of tool_use blocks.
"kimi-coding": {
anthropicToolSchemaMode: "openai-functions",
anthropicToolChoiceMode: "openai-string-modes",
preserveAnthropicThinkingSignatures: false,
},
mistral: {

Some files were not shown because too many files have changed in this diff Show More