Compare commits

...

170 Commits

Author SHA1 Message Date
Peter Steinberger
eeef486449 test(release): align stable onboarding npm prompt 2026-05-07 17:30:09 +01:00
Peter Steinberger
c5c7d102db chore(release): set stable package version 2026-05-07 17:12:34 +01:00
Peter Steinberger
db1ee678ac fix(release): clear final validation blockers 2026-05-07 16:53:25 +01:00
Peter Steinberger
bccf2d6a62 chore(release): set beta package version 2026-05-07 15:48:55 +01:00
Peter Steinberger
001a89ec00 test(discord): type voice capture stream mock 2026-05-07 15:32:47 +01:00
Peter Steinberger
6b4a7d844b fix(release): remove stale unused symbols 2026-05-07 15:24:59 +01:00
Peter Steinberger
e1ac32b4ae fix(plugins): type legacy peer install args 2026-05-07 15:18:17 +01:00
Peter Steinberger
8c667d1ab0 chore(release): set version to 2026.5.7 2026-05-07 15:13:09 +01:00
Peter Steinberger
cb2fa4affb ci(release): speed up beta publish path
(cherry picked from commit a68ad39877)
2026-05-07 15:05:38 +01:00
Peter Steinberger
803ba3ded3 chore(release): set version to 2026.5.6 2026-05-07 12:31:15 +01:00
Peter Steinberger
06809c45fc fix(doctor): preserve Codex route repair sequencing 2026-05-07 12:26:54 +01:00
Shakker
7de45e0dfd fix: handle mixed Codex OAuth PI recovery 2026-05-07 12:24:26 +01:00
Shakker
7b53e58670 fix: preserve Codex OAuth doctor routes 2026-05-07 12:23:34 +01:00
Pavan Kumar Gondhi
5507a1db76 fix(auto-reply): gate inline skill tool dispatch [AI] (#78517)
* fix: enforce tool hooks for inline skill dispatch

* addressing claude review

* addressing codex review

* addressing codex review

* fix: complete root-cause handling

* docs: add changelog entry for PR merge
2026-05-07 11:50:24 +01:00
Christof
f6415d51c3 fix: clear reset skills snapshot (#78873) 2026-05-07 11:48:11 +01:00
Pavan Kumar Gondhi
cd070c2a49 Honor owner enforcement for native commands [AI] (#78864)
* fix: honor owner enforcement for native commands

* addressing codex review

* addressing codex review

* docs: add changelog entry for PR merge
2026-05-07 11:47:31 +01:00
Pavan Kumar Gondhi
543dbd179b fix(active-memory): require admin scope for global toggles [AI] (#78863)
* fix: gate active-memory global writes by admin scope

* addressing claude review

* docs: add changelog entry for PR merge
2026-05-07 11:46:51 +01:00
Alex Knight
84a0015973 fix(compaction): ignore metadata bytes in preflight pressure
Fix stale preflight compaction pressure estimation so metadata bytes before the latest usage record do not count as model-context tokens, while preserving post-usage tail pressure and the active transcript byte guard.

Fixes #78604.
2026-05-07 11:46:09 +01:00
Vincent Koc
a17876c9ba feat(openai): add chat-latest model override
Add openai/chat-latest as an explicit direct API-key OpenAI model override, document the moving alias, and normalize unsupported Responses text verbosity for that model.
2026-05-07 11:44:55 +01:00
Vincent Koc
344f56a23f fix(plugins): share npm script shell env (#78887) 2026-05-07 11:30:59 +01:00
Vincent Koc
88752d9359 changelog: credit @sliverp for channels list channel-only rework 2026-05-07 11:29:43 +01:00
Sliverp
2db4d779db Feat/channels list show all and drop auth (#78456)
* feat(channels list): drop auth providers, add --all, surface installed/configured/enabled

`openclaw channels list` used to conflate two very different surfaces: chat
channels and OAuth/API-key auth providers for model routing. The auth
section was the first and most visible block in the output even for
operators who only cared about chat channels, and its JSON `auth` key
leaked model-provider identities into a command whose top-level help
describes it as channel management. Worse, the command silently hid
every channel that had no configured account, so users could not tell
from `channels list` which bundled or catalog channels were even
available to configure.

Split the surface cleanly around channels only:

1. Remove the `Auth providers (OAuth + API keys)` text section and the
   `auth` field from the JSON payload. Model-provider auth profiles
   remain reachable via `openclaw models auth list`, which is where
   they conceptually belong.

2. Add a `--all` flag to surface every channel an operator could
   configure: bundled channel plugins that have no account yet and
   catalog-listed external channels whose plugin package is not even
   installed on disk. Without `--all` the output still shows only
   channels with at least one configured account, matching the
   previous default behavior so existing scripts keep working. The
   "empty" default path now prints a hint pointing at `--all`.

3. Render three explicit status tags per row — `installed` /
   `not installed`, `configured` / `not configured`, `enabled` /
   `disabled` — so bundled-but-unconfigured plugins and installable
   catalog channels both render with accurate state instead of being
   invisible. Installed state comes from the same
   `isCatalogChannelInstalled` probe the setup flow uses, so it stays
   consistent with `openclaw onboard` and `channels add`.

4. JSON payload now carries an `origin` per channel (`configured`,
   `available`, `installable`) alongside `installed: boolean`, which
   lets tooling distinguish "user has set this up" from "user could
   set this up" without second-guessing.

Register `--all` on both the Commander CLI and the fast-path route-arg
parser so the flag works in both code paths, update the one routes
test that asserted the parsed args shape, and rewrite the old auth
profiles surface test as a broader `channels list` behavior spec
covering default output, `--all` output, JSON shape (no `auth`), and
the bundled-unconfigured + catalog-not-installed cases.

Docs: call out that `channels list` is chat-channel only now, mention
`--all`, and point at `openclaw models auth list` for what used to be
the auth providers block.

* fix(channels list): surface catalog channels that are installed on disk but not yet configured

The previous `--all` path filtered catalog entries with
`!installedByChannelId.get(entry.id)` before rendering them as
catalog-only rows. That assumed "catalog entry not already rendered
as a plugin row" implied "not installed", which is wrong: an external
channel plugin package can be installed on disk (`isCatalogChannelInstalled`
returns true) while the read-only channel loader still declines to
surface a plugin object for it — the loader only activates channels
that appear in user config, so a plugin that is installed but never
configured ended up in neither bucket and silently dropped out of
`channels list --all`.

Operator-facing symptom: `pnpm openclaw channels list --all` omitted
WeCom (and any other catalog channel in the same state) even though
its npm package was present on disk and its catalog entry existed,
while rendering every other uninstalled catalog channel as expected.

Fix: drop the `installed` filter from `catalogOnlyLines` so every
catalog entry that is not already represented by a plugin row is
rendered, and let the row itself carry the real installed/not-installed
tag. Two renderings now land in the catalog-only bucket:

- Not installed — rendered as `not installed, not configured, disabled`
  (installable row).
- Installed but unconfigured — rendered as `installed, not configured,
  disabled` (ready-to-configure row). The JSON `origin` for this case
  becomes `available`, matching the existing origin for bundled
  plugins that are installed but unconfigured, so downstream tooling
  sees a consistent "you could configure this now" signal regardless
  of whether the plugin came from bundled sources or from the catalog.

Regression test added under the WeCom scenario.

* refactor(channels list): drop model-provider usage surface, make the command channel-only

`openclaw channels list` used to append a model-provider usage/quota
snapshot (Anthropic, OpenRouter, OpenAI Codex, Gemini, Zai, Minimax,
etc.) under every invocation. That was a leftover from the days when
`channels list` was the only "operator overview" command; the same
data is now owned by `openclaw status` (overview) and
`openclaw models list` (per-provider), which handle timeouts, probe
errors, and output shape consistently for that class of data. Keeping
the snapshot wired into `channels list` meant:

- Every default invocation made one blocking `loadProviderUsageSummary`
  call that fanned out to every configured provider billing/auth
  endpoint, adding seconds of latency to a command that otherwise
  just reads local config.
- `channels list --no-usage` was the escape hatch, but the flag was
  itself a self-sustaining bug: it only existed because the command
  did work that did not belong to it.
- JSON consumers had an optional `usage` key whose shape was owned by
  the provider-usage module, not by the channels module, so any
  change upstream silently reshaped `channels list --json` output.
- Failed provider fetches printed provider-side errors on a command
  that never advertised itself as a provider-health surface.

Scope this PR tightens, in one move:

1. Remove `loadProviderUsageSummary` / `formatUsageReportLines` usage
   from `src/commands/channels/list.ts`. The command now only reads
   config, the read-only channel plugin registry, and the trusted
   catalog — matching its name.
2. Drop `--no-usage` from the Commander CLI registration, from the
   fast-path route-arg parser (`parseChannelsListRouteArgs`), and
   from `ChannelsListOptions`. The flag is gone, not silently
   ignored, so anyone depending on it will get a clear
   "unknown option" from Commander and from the fast-path router.
3. Drop the `usage` key from `channels list --json` payloads. Shape
   of the `chat` record and the new `origin` / `installed` tags
   introduced earlier in this branch are unchanged.
4. Print a single-line migration pointer at the bottom of the text
   output so operators who expected usage know where it went
   (`openclaw status` / `openclaw models list`). This replaces what
   used to be a block of fetched provider data with one static line,
   so it cannot fail or add latency.
5. Update `docs/cli/channels.md` troubleshooting to remove the
   `--no-usage` mention and point at the two new entry points.
6. Update tests: drop the `loadProviderUsageSummary` mock and the
   `"keeps JSON output valid when usage loading fails"` case,
   replace it with a positive assertion that `payload.usage` is
   undefined (locking in the narrower contract), and remove `usage`
   from every `channelsListCommand(...)` call to match the narrowed
   `ChannelsListOptions` type. The route-args test is updated to
   expect `{ json, all }` without `usage`.

No other command changes. `openclaw status` and `openclaw models list`
already render usage; they are the documented replacements.

Breaking-ish surface:

- CLI: `channels list --no-usage` now fails with "unknown option".
  Tooling should drop the flag — there is nothing left to opt out of.
- JSON: `channels list --json` no longer carries a top-level `usage`
  key. Tooling that read it must migrate to
  `openclaw status --json` or `openclaw models list --json`.

* fix(channels.list.test): widen isCatalogChannelInstalled mock signature to accept entry param

CI typecheck failed because the mock was declared with a zero-arg signature while one test called mockImplementation(({ entry }) => …). Tighten the generic so vitest's mock accepts the same params the real helper does.

* changelog: record channels list channel-only rework (#78456)
2026-05-07 11:28:52 +01:00
VACInc
8737b1860e Fix Tavily tool SecretRef runtime config
Resolve Tavily dedicated tool credential lookup against the active runtime config snapshot.

PR: https://github.com/openclaw/openclaw/pull/78610
2026-05-07 11:27:48 +01:00
Vincent Koc
edbc6f4d49 docs(cli): document cron list/show --json status field 2026-05-07 11:26:54 +01:00
Aaron Weiker
105242f06d feat(cron): add computed status field to --json output (#78701)
* feat(cron): add computed status field to --json output

`openclaw cron list --json` and `openclaw cron show <id> --json` now
include a top-level `status` field on each job object, computed from
enabled + state.runningAtMs + state.lastRunStatus.

Values: "disabled" | "running" | "ok" | "error" | "skipped" | "idle"

This matches the human-readable status column already shown by
`cron list` and `cron show` (without --json), making it easier for
external tooling (dashboards, ops gateways) to determine job state
without re-implementing the derivation logic.

The raw state object is preserved unchanged for backward compatibility.

* fix: preserve lastStatus fallback + add changelog entry

Address ClawSweeper review findings:
- P2: Fall back to deprecated state.lastStatus when lastRunStatus is
  absent, matching the existing formatStatus behavior for legacy jobs.
- P3: Add CHANGELOG.md entry under Unreleased for this user-facing
  CLI feature.

* fix: address lint errors - add braces and avoid spread-in-map

---------

Co-authored-by: Rodin <rodin@forgedthought.ai>
Co-authored-by: claw <claw@weiker.me>
2026-05-07 11:25:51 +01:00
Rajvardhan Patil
18ebbe3f36 fix(btw): keep usage placeholder visible
Fixes #62877.\n\nThanks @RajvardhanPatil07.
2026-05-07 11:24:25 +01:00
pashpashpash
6c1f92ed88 fix: refresh plugin approval protocol models
(cherry picked from commit a4d6378a41)
2026-05-07 11:14:05 +01:00
pashpashpash
5e9e645458 fix: let codex guardian own native permission approvals
(cherry picked from commit fcfdf02b16)
2026-05-07 11:14:05 +01:00
Shakker
f0bc87def4 fix: scope native approval reuse by session
(cherry picked from commit a758b5e99c)
2026-05-07 11:12:34 +01:00
Shakker
4b8f9433b8 fix: reuse codex native approvals
(cherry picked from commit f1bf2a80a6)
2026-05-07 11:12:34 +01:00
Peter Steinberger
1ca61d1b8c fix(whatsapp): adapt media dedupe to release delivery path 2026-05-07 11:02:28 +01:00
Marcus Castro
04ccb48164 docs: note whatsapp media directive dedupe
(cherry picked from commit 54abdd786d)
2026-05-07 11:00:30 +01:00
ai-hpc
1141b4588f fix(whatsapp): dedupe captioned MEDIA auto-replies
(cherry picked from commit 1ede151ad0)
2026-05-07 10:59:37 +01:00
Sliverp
f204c1c390 fix(onboard): recover externalized channel plugin from stale config (#78328)
When a user's config has a stale `channels.<id>` entry (e.g. `appId`
or tokens left over from an earlier install) and the plugin is no
longer on disk -- for instance because the externalized npm package
was uninstalled or pruned during an upgrade -- `handleChannelChoice`
used to dead-end with "<channel> plugin not available." and leave
onboard stuck until the user manually deleted the config entry and
re-ran the CLI.

Two discovery paths are affected:

1. The `installedCatalogEntry` branch: when
   `loadScopedChannelPlugin` returns null but the catalog entry still
   carries `install.npmSpec`, fall back to
   `ensureChannelSetupPluginInstalled` with the same entry so onboard
   can reinstall the plugin from the official catalog.

2. The bundled-enable `else` branch: with a non-empty
   `channels.<id>` record, `isStaticallyChannelConfigured` drops the
   channel from `installableCatalogEntries`; if the plugin is also
   missing on disk (so it never enters `manifestInstalledIds`), both
   discovery buckets come back empty and the channel falls through to
   `enableBundledPluginForSetup`. Before delegating to that bundled
   path, consult the trusted catalog via
   `getTrustedChannelPluginCatalogEntry` and, if an `install.npmSpec`
   is available, drive the same catalog install flow used by a fresh
   pick of the channel.

Both new fallbacks re-apply the `resolveConfigDisabledHint` guard
that `enableBundledPluginForSetup` has always enforced, so an
operator-disabled channel (`plugins.entries.<id>.enabled === false`
or explicit `channels.<id>.enabled === false`) with a stale config
entry cannot be silently reinstalled or re-enabled through the
catalog path.

Both branches also keep their previous behavior when no catalog npm
spec is available (e.g. purely bundled channels), so this change is
a superset of the old flow rather than a replacement.

Affects all externalized channel plugins listed in the core
package's `files` exclusion (qqbot, bluebubbles, discord, whatsapp,
line, msteams, feishu, googlechat, nostr, zalo, zalouser,
synology-chat, tlon, twitch, and similar).

(cherry picked from commit 329580c64d)
2026-05-07 09:27:01 +01:00
sallyom
e7e5de1c01 fix: persist rotated gateway session files
Signed-off-by: sallyom <somalley@redhat.com>
(cherry picked from commit bf2511098f)
2026-05-07 07:35:50 +01:00
Eden
d74cf76be9 fix(whatsapp): resolve outbound PN to LID via auth-dir forward mapping (#74925)
Merged via squash.

Prepared head SHA: 5f51cb7cb3
Co-authored-by: edenfunf <146086744+edenfunf@users.noreply.github.com>
Co-authored-by: mcaxtr <7562095+mcaxtr@users.noreply.github.com>
Reviewed-by: @mcaxtr

(cherry picked from commit fcdfa30703)
2026-05-07 07:18:33 +01:00
Vincent Koc
bfc12cb0da fix(discord): smooth voice capture prompts
(cherry picked from commit 6e5ba8b047)
2026-05-07 07:17:13 +01:00
Peter Steinberger
8e26a76bf0 fix(discord): audit voice channel permissions
(cherry picked from commit a4d7206558)
2026-05-07 07:15:57 +01:00
Peter Steinberger
6b57ec27f0 fix(gemini): gate thought-signature replay trust
(cherry picked from commit a428568157)
2026-05-07 07:11:50 +01:00
Peter Steinberger
56e6a0e735 fix(model): repair provider replay edge cases
(cherry picked from commit 85b914a4e1)
2026-05-07 07:11:32 +01:00
Chunyue Wang
8da8a0b24b fix(plugins): forward setChannelRuntime from non-bundled external setup entries (#77799)
Merged via squash.

Prepared head SHA: 7b7676be0d
Co-authored-by: openperf <80630709+openperf@users.noreply.github.com>
Co-authored-by: openperf <80630709+openperf@users.noreply.github.com>
Reviewed-by: @openperf

(cherry picked from commit 42a32298f9)
2026-05-07 07:07:30 +01:00
Peter Steinberger
bb9e318b3a fix(openrouter): canonicalize auto selector refs
(cherry picked from commit 610e882dbf)
2026-05-07 07:06:37 +01:00
Peter Steinberger
59d6d47f93 fix(cli): normalize heic model-run files
(cherry picked from commit 32c1356926)
2026-05-07 07:06:37 +01:00
Peter Steinberger
95577906da fix(openai): honor embedding output dimensions
(cherry picked from commit ea116ca36e)
2026-05-07 07:05:46 +01:00
Peter Steinberger
bbb60f6f2c fix(anthropic): reject uppercase dynamic model ids
(cherry picked from commit f37fba8d5a)
2026-05-07 07:05:46 +01:00
Vincent Koc
44be32703e fix(commands): audit explicit task records
(cherry picked from commit 70defcc046)
2026-05-07 07:05:46 +01:00
Peter Steinberger
c768975ef5 fix: bound stale task reload blockers
(cherry picked from commit 6009b86f0d)
2026-05-07 07:05:46 +01:00
Neerav Makwana
1181698892 fix(telegram): avoid fallback after message tool send (#78726) (thanks @neeravmakwana)
* telegram: correlate message-tool sends with inbound turn (#78685)

Register the active Telegram inbound SessionKey/outbound peer while dispatching,
and mark inbound lane delivery when deliverOutbound emits a matching telegram
message:sent success. Prevents rewritten silent-reply fillers after visible
tool-routed replies with an empty final.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(telegram): track message action delivery

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
(cherry picked from commit 447182a852)
2026-05-07 07:04:58 +01:00
Arnab Saha
e0f840a869 fix(subagents): honor archiveAfterMinutes for session-mode reaping (#78263)
Merged via squash.

Prepared head SHA: b415467008
Co-authored-by: arniesaha <3646287+arniesaha@users.noreply.github.com>
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Reviewed-by: @jalehman

(cherry picked from commit 1c331a814a)
2026-05-07 07:02:47 +01:00
NVIDIAN
3b05c2e897 fix(telegram): keep polling watchdog on getUpdates liveness (#78646)
(cherry picked from commit 440111ff6f)
2026-05-07 07:01:52 +01:00
Peter Steinberger
d1f71fc117 fix(agents): cap live exec update payloads
(cherry picked from commit b7d0d92600)
2026-05-07 01:36:20 +01:00
Peter Steinberger
3f08aad2b6 fix(delivery): require outbound send result for success
(cherry picked from commit 372e270871)
2026-05-07 01:26:10 +01:00
Peter Steinberger
466ec818a7 fix(telegram): honor access group allowlists
(cherry picked from commit b6ae0b83a6)
2026-05-07 01:25:33 +01:00
Kevin Lin
20251902e0 fix(cron): repair stale future next-run slots (#78272)
* fix(cron): repair stale future next-run slots

* fix(cron): repair stale future next-run slots

* fix(cron): repair stale future next-run slots

* fix(cron): repair stale future next-run slots

* fix(cron): repair stale future next-run slots

* fix(cron): repair stale future next-run slots

* fix(cron): repair stale future next-run slots

(cherry picked from commit 7175b1b5c6)
2026-05-07 01:05:35 +01:00
brokemac79
437ce8d5a1 [AI-assisted] fix(agents): invalidate context engine cache (#78163)
Merged via squash.

Prepared head SHA: 6ed3add797
Co-authored-by: brokemac79 <255583030+brokemac79@users.noreply.github.com>
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Reviewed-by: @jalehman

(cherry picked from commit 609a5d70a5)
2026-05-07 01:05:26 +01:00
Peter Steinberger
9d68ac9f50 fix: reuse scoped provider auth metadata 2026-05-07 00:47:58 +01:00
Kevin Lin
76b0be61c9 fix(cron): repair bad persisted model sentinels (#78641)
* fix(cron): repair bad persisted model sentinels

* test(cron): relax model preservation assertion

(cherry picked from commit 6aafdf121a)
2026-05-07 00:45:17 +01:00
Josh Avant
533826780a fix gateway optional plugin startup (#78642)
(cherry picked from commit 5572ee1a1a)
2026-05-07 00:44:43 +01:00
adzendo
67dcc9e8bd fix: clamp compaction max_tokens to model output limit (#54392)
Merged via squash.

Prepared head SHA: 8a888213ed
Co-authored-by: adzendo <246828680+adzendo@users.noreply.github.com>
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Reviewed-by: @jalehman

(cherry picked from commit ac43135984)
2026-05-07 00:43:46 +01:00
Patrick Erichsen
b412d7056e fix(discord): parse provider-prefixed channel targets (#78625)
* fix(discord): parse provider-prefixed channel targets

* fix(discord): resolve allowlisted numeric dm targets

(cherry picked from commit 51356620e9)
2026-05-07 00:43:17 +01:00
Shakker
2c8e68cf1f fix: preserve provider auth alias scope
(cherry picked from commit bc97182d71)
2026-05-07 00:42:47 +01:00
Peter Steinberger
3e34133137 fix(ci): reject mixed-case staging debris before pack
(cherry picked from commit c738539b1e)
2026-05-07 00:42:42 +01:00
Vincent Koc
983c546eac fix(ci): keep cross-os matrix resolution dependency-free
(cherry picked from commit d70e06334e)
2026-05-07 00:42:42 +01:00
Peter Steinberger
dbf185575a fix(plugins): reconcile managed npm uninstall backports 2026-05-06 22:30:25 +01:00
Peter Steinberger
38989bdceb fix(infra): avoid redundant usage summary refresh
(cherry picked from commit 2daf3d332f)
2026-05-06 22:27:12 +01:00
Vincent Koc
36bdb4b15a fix(docker): preserve functional image dependencies
(cherry picked from commit 445dda54f1)
2026-05-06 22:27:12 +01:00
Vincent Koc
2c8a1c3cdb fix(docker): avoid package node_modules copy conflicts
(cherry picked from commit ffd212ca43)
2026-05-06 22:27:12 +01:00
Peter Steinberger
8a84958320 fix: clean stale managed npm plugin deps on uninstall
(cherry picked from commit 858038320d)
2026-05-06 22:27:12 +01:00
Peter Steinberger
e4ee1e5b57 fix: skip npm uninstall when managed root is absent
(cherry picked from commit 3e8b5b4ee7)
2026-05-06 22:13:26 +01:00
Peter Steinberger
97305d2518 fix(discord): keep agents quiet when others are addressed (#78615)
* fix(discord): keep agents quiet when others are addressed

* fix(groups): tighten addressed-elsewhere prompt

(cherry picked from commit 9c7c0ae891)
2026-05-06 21:43:51 +01:00
Peter Steinberger
e30ffd0853 fix(discord): prompt agents to suppress link embeds (#78614)
* fix(discord): prompt agents to suppress link embeds

* fix(discord): tighten link embed prompt

(cherry picked from commit 197edaa33d)
2026-05-06 21:43:51 +01:00
Peter Steinberger
58a5fc3895 fix(plugins): preserve legacy peer mode in managed npm installs 2026-05-06 21:11:03 +01:00
Vincent Koc
88503674af fix(plugins): skip managed npm peer resolution (#78348)
(cherry picked from commit aa9247e0ce)
2026-05-06 21:09:54 +01:00
Vincent Koc
cbf08f16d5 fix(plugins): apply npm overrides to managed roots (#78386)
(cherry picked from commit 5d557171b3)
2026-05-06 21:08:22 +01:00
Peter Steinberger
acab03a594 chore(release): prepare 2026.5.7 2026-05-06 21:01:15 +01:00
Peter Steinberger
e75480dbb8 fix(release): harden ClawHub plugin publish 2026-05-06 20:59:31 +01:00
Peter Steinberger
c97b9f79ec test(plugin-sdk): satisfy fetch header lint 2026-05-06 17:58:50 +01:00
Peter Steinberger
1ec03a9538 chore(release): refresh config baseline hash 2026-05-06 17:47:51 +01:00
Ayaan Zaidi
8738f9e772 test(net): cover stalled dispatcher close after fetch timeout
(cherry picked from commit b559fce7a1)
2026-05-06 17:43:58 +01:00
Peter Steinberger
c4d3026a1e docs(release): note web fetch timeout cleanup 2026-05-06 17:42:17 +01:00
Ayaan Zaidi
92339752ea fix(net): bound guarded fetch dispatcher cleanup
(cherry picked from commit 2465217b23)
2026-05-06 17:41:17 +01:00
Peter Steinberger
623757011e Revert "fix(doctor): repair legacy Codex route config"
This reverts commit 258e153705.

(cherry picked from commit c8fcc46978c5b87f0149e3b1727d8a4ce36c72c5)
2026-05-06 17:32:49 +01:00
Peter Steinberger
24e8152a0c docs(release): trim 2026.5.6 changelog 2026-05-06 17:29:12 +01:00
Peter Steinberger
9d0dbe0754 chore(release): update generated config schema version 2026-05-06 17:25:42 +01:00
Peter Steinberger
ee1d5cc566 chore(release): bump version to 2026.5.6 2026-05-06 17:23:45 +01:00
Shakker
61c22a6eab fix: normalize debug proxy fetch headers
(cherry picked from commit 96f80fa3ff)
2026-05-06 17:22:14 +01:00
Shakker
1c724f4861 fix: normalize symbolic fetch headers
(cherry picked from commit 79f21a4442)
2026-05-06 17:22:03 +01:00
Peter Steinberger
680064bf98 ci: fix release cross-os loader path 2026-05-06 12:20:38 +01:00
Peter Steinberger
65ce07547a ci: harden release validation harness checks 2026-05-06 12:06:01 +01:00
Peter Steinberger
982c8d8654 ci: cap native MiniMax release live gateway lane 2026-05-06 11:45:17 +01:00
Peter Steinberger
b25dc1704f ci: narrow MiniMax release live gateway lane 2026-05-06 11:10:15 +01:00
Peter Steinberger
7615b425c5 fix(release): stabilize final validation checks 2026-05-06 10:18:12 +01:00
Peter Steinberger
a162aafe02 fix(release): tolerate optional plugin beta tag mirror failure 2026-05-06 09:39:22 +01:00
Peter Steinberger
b1abf9d8ae chore(release): refresh base config schema 2026-05-06 09:12:30 +01:00
Peter Steinberger
665b164d4f test(line): narrow open dm policy parse result 2026-05-06 08:58:51 +01:00
Peter Steinberger
4364d77442 chore(release): prepare 2026.5.5 final 2026-05-06 08:49:48 +01:00
Peter Steinberger
50488f9f5f chore(matrix): remove stale extension changelog 2026-05-06 08:47:05 +01:00
Peter Steinberger
430c0bdaba fix(line): require wildcard for open dm policy 2026-05-06 08:43:38 +01:00
keshavbotagent
9d3dcfdd51 fix: show Codex tool progress in channel drafts (#77949)
Summary:
- Normalize Codex app-server dynamic and native tool activity into channel-visible tool progress.
- Keep Telegram message-tool-only progress drafts visible without duplicate dynamic item/tool lines.
- Preserve suppressed item progress while avoiding duplicate tool callbacks.

Verification:
- OPENCLAW_VITEST_MAX_WORKERS=1 pnpm test extensions/codex/src/app-server/event-projector.test.ts extensions/codex/src/app-server/run-attempt.test.ts extensions/telegram/src/bot-message-dispatch.test.ts src/auto-reply/reply/agent-runner-execution.test.ts src/auto-reply/reply/dispatch-from-config.test.ts --pool=forks --maxWorkers=1
- pnpm tsgo:extensions:test
- pnpm exec oxfmt --check --threads=1 CHANGELOG.md extensions/codex/src/app-server/event-projector.ts extensions/codex/src/app-server/event-projector.test.ts extensions/codex/src/app-server/run-attempt.ts extensions/codex/src/app-server/run-attempt.test.ts extensions/codex/src/app-server/tool-progress-normalization.ts extensions/telegram/src/bot-message-dispatch.ts extensions/telegram/src/bot-message-dispatch.test.ts src/auto-reply/get-reply-options.types.ts src/auto-reply/reply/agent-runner-execution.ts src/auto-reply/reply/agent-runner-execution.test.ts src/auto-reply/reply/dispatch-from-config.ts src/auto-reply/reply/dispatch-from-config.test.ts src/infra/agent-events.ts
- pnpm lint:extensions
- pnpm build
- CI on 6ff6a1f868: 88 success, 20 skipped, 1 neutral, no failures or pending checks

Fixes #75641.
2026-05-06 08:40:48 +01:00
Peter Steinberger
5932467c0c chore(release): prepare 2026.5.5 beta 2 2026-05-06 07:48:03 +01:00
Peter Steinberger
62840687ed fix(feishu): keep topic sessions stable
Fixes Feishu native topic starter routing by hydrating a missing topic thread ID before session resolution.\n\nCloses #78262.

(cherry picked from commit 8cc762daff)
2026-05-06 07:37:50 +01:00
Peter Steinberger
74ab84454d fix(plugins): repair managed npm openclaw peers
Remove stale managed-root openclaw manifests, locks, hidden locks, and installed copies before npm plugin installs.

Relink plugin-local openclaw peer symlinks after shared-root npm install, rollback, update, and uninstall mutations so SDK-using plugins keep resolving openclaw/plugin-sdk/*.

Force safe npm commands out of inherited legacy/strict peer-dependency modes.

Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
Co-authored-by: Patrick Erichsen <patrick.a.erichsen@gmail.com>
(cherry picked from commit 8e533490ab)
2026-05-06 07:36:57 +01:00
Peter Steinberger
d9dcade46c test: pass env to fallback metadata snapshot
(cherry picked from commit be1c99b76a)
2026-05-06 06:29:22 +01:00
Val Alexander
6968e18998 fix(sessions): restore Control UI /new hooks
Fixes #76957.

Restores the Control UI /new hook lifecycle through an explicit sessions.create emitCommandHooks opt-in, preserving hook-free defaults for programmatic parent-session creates.

Validation:
- pnpm protocol:check
- pnpm test src/gateway/server.sessions.reset-hooks.test.ts ui/src/ui/app-render.helpers.node.test.ts
- pnpm exec oxlint on touched TS files
- pnpm exec oxfmt --check --threads=1 on touched files
- git diff --check
- OPENCLAW_LOCAL_CHECK=1 OPENCLAW_LOCAL_CHECK_MODE=throttled env NODE_OPTIONS=--max-old-space-size=4096 pnpm check:changed
- GitHub PR checks green on 3a446ec78e
- ClawSweeper re-review completed with no blocking findings and security cleared

Duplicate triage:
- #77376, #77004, and #76967 were superseded closed attempts for #76957
- #77562 is a closed duplicate issue
- #77880 mentions #76957 but is not a duplicate of this hook fix

(cherry picked from commit 49c4a13231)
2026-05-06 04:58:57 +01:00
Peter Steinberger
39b6c580cf fix(discord): route guild text commands (#78080)
(cherry picked from commit d7bd9fe049)
2026-05-06 04:58:19 +01:00
Peter Steinberger
dcac9998da fix: cap memory wiki filenames for safe writes
(cherry picked from commit ebb8bed78f)
2026-05-06 04:58:15 +01:00
Bryce D. Greybeard
de6e2616a1 fix(discord): avoid false heartbeat ACK timeouts
Fix the Discord Gateway heartbeat scheduler so ACK timeout checks are measured from the actual heartbeat send, not from the fixed HELLO-time interval. This prevents late randomized first heartbeats from causing false reconnect loops while the Discord channel is still awaiting readiness.\n\nVerification:\n- pnpm test extensions/discord/src/internal/gateway-lifecycle.test.ts extensions/discord/src/internal/gateway.test.ts\n- pnpm exec oxfmt --check --threads=1 CHANGELOG.md extensions/discord/src/internal/gateway-lifecycle.ts extensions/discord/src/internal/gateway-lifecycle.test.ts extensions/discord/src/internal/gateway.test.ts\n- git diff --check\n- Real behavior proof check passed on PR head bf239b886020c11d55af33f16674e953535f9b4c\n\nFixes #77668.\nSupersedes #77956.\nThanks @bryce-d-greybeard and @NikolaFC.

(cherry picked from commit b5c33bc204)
2026-05-06 04:53:05 +01:00
Peter Steinberger
013a2c50f6 chore(release): refresh release metadata for 2026.5.5 beta 1 2026-05-06 03:03:03 +01:00
Peter Steinberger
6896bd3ddf chore(release): keep 2026.5.5 beta 1 2026-05-06 02:50:50 +01:00
Peter Steinberger
9cb0b97f74 docs(release): clarify unpublished beta tag movement 2026-05-06 02:48:55 +01:00
Peter Steinberger
e820821950 chore(release): prepare 2026.5.5 beta 2 2026-05-06 02:45:51 +01:00
Peter Steinberger
ecf2dc58e1 chore(release): prepare 2026.5.5 beta 1 2026-05-06 02:39:25 +01:00
Vincent Koc
817b56f22d fix(cli): repair legacy config before update channel switch (#77069)
* fix(cli): repair legacy config before update channel switch

* docs(changelog): note update channel legacy config repair

* fix(update): keep legacy config repair doctor-owned

* fix(update): keep dry runs read-only

* fix(update): avoid include-flattening legacy repair
2026-05-06 02:31:02 +01:00
Vincent Koc
258e153705 fix(doctor): repair legacy Codex route config
Repair legacy openai-codex route config and session pins safely.
2026-05-06 02:31:02 +01:00
Vincent Koc
acdf0e432a fix(plugins): sync official plugin installs during update (#78065)
* fix(plugins): sync official npm installs during update

* fix(plugins): sync official clawhub installs during update

* test(update): mock official plugin sync helpers

---------

Co-authored-by: Patrick Erichsen <patrick.a.erichsen@gmail.com>
2026-05-06 02:30:17 +01:00
Vincent Koc
3c04d7e710 fix(video): recover generation parameter fallbacks 2026-05-06 02:29:39 +01:00
Vincent Koc
1205c9ef1f feat(status): show uptime in chat status
Show compact Gateway process and host system uptime in chat /status output.
2026-05-06 02:29:03 +01:00
Vincent Koc
ba5bc48f70 fix(gateway): keep reset and refresh paths responsive (#77701)
* fix(hooks): keep session memory slugging off reset hot path

* fix(hooks): run session memory capture asynchronously

* fix(cli): avoid stuck gateway command exits

* fix(gateway): cache empty read-only model catalog

* fix(doctor): stop stale TUI clients for WhatsApp responsiveness
2026-05-06 02:28:25 +01:00
Vincent Koc
cbbdaf92a4 fix(hooks): avoid session memory filename collisions
Add collision suffixes for session-memory fallback filenames so repeated same-minute reset/new captures do not overwrite earlier archives.
2026-05-06 02:27:46 +01:00
Vincent Koc
1692c84b8c test(live): classify provider HTTP 5xx as server drift 2026-05-06 02:27:11 +01:00
Vincent Koc
0097427d08 fix(tui): bound session list recency (#77752) 2026-05-06 02:27:11 +01:00
Vincent Koc
2d2fc19e36 fix(core): avoid session export filename collisions (#77762) 2026-05-06 02:26:39 +01:00
Vincent Koc
daff8916de fix(agents): filter runtime context from context engines
- filter hidden runtime-context custom messages before context-engine assemble, afterTurn, and ingest fallback hooks
- preserve the pre-prompt/new-turn boundary after filtering
- add regression coverage for assemble, afterTurn, and ingestBatch fallback behavior

- pnpm test:serial src/agents/harness/context-engine-lifecycle.test.ts -- --reporter=verbose
- pnpm exec oxfmt --check --threads=1 src/agents/harness/context-engine-lifecycle.ts src/agents/harness/context-engine-lifecycle.test.ts CHANGELOG.md
- git diff --check origin/main...HEAD
- pnpm changed:lanes --json
- pnpm testbox:run --id tbx_01kqx8fy1ktpqczkcej2pgpryz -- "OPENCLAW_TESTBOX_REMOTE_RUN=1 pnpm check:changed"
2026-05-06 02:11:24 +01:00
Vincent Koc
3569edb38e fix(tui): prevent orphaned terminal sessions (#77662)
* fix(tui): prevent orphaned terminal sessions

* fix(doctor): repair heartbeat-poisoned main sessions

* fix(tui): preserve startup tls respawn

* fix: harden tui and doctor recovery paths
2026-05-06 02:10:54 +01:00
Eden
9cc6cca75d fix(gateway): improve shutdown error visibility and add close timeout
Adds structured warning collection to gateway shutdown, preserves lifecycle timeout handling, and covers HTTP/WebSocket/subsystem warning paths.

Co-authored-by: Eden <146086744+edenfunf@users.noreply.github.com>
Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
2026-05-06 02:10:21 +01:00
Vincent Koc
e8ad813282 fix(update): avoid lint-blocked dev installs (#77181) 2026-05-06 02:08:59 +01:00
Vincent Koc
e8f0608b09 fix(cli): fast-path bare channels help (#77659)
* fix(cli): fast-path bare channels help

* fix(cli): normalize channels add argv gating

* fix(cli): restore channel add completion flags
2026-05-06 02:08:26 +01:00
Vincent Koc
7470ea9073 fix(gateway): cancel delayed maintenance on shutdown 2026-05-06 02:07:54 +01:00
Vincent Koc
ed4ed5aead fix(status): show runtime in CLI sessions (#77776)
* fix(status): show agent runtime in cli status

* fix(status): preserve configured runtime labels
2026-05-06 02:07:21 +01:00
Vincent Koc
3544ef0afa fix(sessions): show runtime in sessions table 2026-05-06 02:06:51 +01:00
Vincent Koc
7da737c67d fix(ui): show session runtime in sessions table 2026-05-06 02:06:10 +01:00
Peter Steinberger
95e9e29219 fix(release): tighten xai and corrupt plugin checks 2026-05-06 02:04:00 +01:00
Peter Steinberger
31633bc0ec fix(release): unblock 2026.5.5 validation 2026-05-06 01:06:49 +01:00
Patrick Erichsen
30927c8491 Tolerate corrupt plugins during update (#77706)
* fix(update): tolerate corrupt plugin state

* fix(update): preserve corrupt plugin proof state

* fix(update): narrow corrupt plugin warnings

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
(cherry picked from commit 8aa7b7a4ca)
2026-05-05 23:51:57 +01:00
Peter Steinberger
9a61edb419 fix(discord): show reasoning text in progress drafts (#78050)
* fix(discord): show reasoning text in progress drafts

* fix(discord): handle reasoning progress snapshots

* test: isolate usage-format models fixture

(cherry picked from commit d94e7f5114)
2026-05-05 23:49:57 +01:00
Vincent Koc
e12dbf527f fix(gateway): skip media sidecar for unrelated HTTP routes
(cherry picked from commit d38e30e02c)
2026-05-05 23:49:22 +01:00
Peter Steinberger
23319a3cc2 fix: restore Codex agent dir runtime import
(cherry picked from commit a6d88e3cd9)
2026-05-05 23:48:50 +01:00
Peter Steinberger
82e914e506 fix(gateway): mark openai role chunks unfinished
(cherry picked from commit fd86ab2e50)
2026-05-05 23:48:50 +01:00
Peter Steinberger
c82f66e3c2 fix(gateway): flush initial openai chat stream chunk
(cherry picked from commit d520bc4cb6)
2026-05-05 23:48:50 +01:00
Ayaan Zaidi
191e821b71 fix(update): stop dev updates after fetch failure
(cherry picked from commit c1a385df83)
2026-05-05 23:48:15 +01:00
Chunyue Wang
7a2e7dba73 fix(auth-profiles): exclude format rejections from profile cooldown (#77280)
Merged via squash.

Prepared head SHA: f4188b4dc3
Co-authored-by: openperf <80630709+openperf@users.noreply.github.com>
Co-authored-by: openperf <80630709+openperf@users.noreply.github.com>
Reviewed-by: @openperf

(cherry picked from commit 31da1fe5b0)
2026-05-05 23:48:15 +01:00
Ayu
eda33431de security: harden gateway container privileges
Adds cap_drop and no-new-privileges hardening for the bundled gateway Docker Compose services.\n\nThanks @VintageAyu.

(cherry picked from commit f9da484365)
2026-05-05 23:48:15 +01:00
Peter Steinberger
4aa91b0b97 fix: backport media completion fallback 2026-05-05 23:34:08 +01:00
Peter Steinberger
8a601b0607 fix: avoid media completion fallback while announce pending
(cherry picked from commit b32d4c5255)
2026-05-05 23:31:22 +01:00
Peter Steinberger
6f9f36a38f test: cover generated media delivery evidence fallback
(cherry picked from commit add9a49c40)
2026-05-05 23:28:10 +01:00
Peter Steinberger
c03449678e fix: recognize attachment message sends
(cherry picked from commit a0ea07e462)
2026-05-05 23:27:11 +01:00
Peter Steinberger
edbd3355be chore(release): bump version to 2026.5.5
(cherry picked from commit c37871e77b)
2026-05-05 23:25:59 +01:00
Peter Steinberger
325df3efef chore(release): bump to 2026.5.4 2026-05-05 08:37:19 +01:00
Peter Steinberger
2fc80754cf ci: parallelize release publish workflows 2026-05-05 07:35:28 +01:00
Peter Steinberger
41f028e2ea fix(diagnostics): drop stale session recovery event cases 2026-05-05 06:06:02 +01:00
Peter Steinberger
303ff716d4 chore(release): refresh plugin SDK API baseline 2026-05-05 05:56:41 +01:00
Peter Steinberger
5fcdeae80c chore(release): bump to 2026.5.4-beta.3 2026-05-05 05:51:46 +01:00
6607changchun
b73317c217 fix(sandbox): support Windows drive-letter bind sources
Accept drive-absolute Windows sandbox Docker bind sources in config and runtime validation while keeping blocked-path and allowed-root comparisons case-insensitive for Windows drive paths.

Also remove a stale WhatsApp setup import that blocked extension lint after the rebase.

Co-authored-by: 6607changchun <84566142+6607changchun@users.noreply.github.com>
Co-authored-by: Brad Groux <3053586+BradGroux@users.noreply.github.com>
(cherry picked from commit d02fbc6116)
2026-05-05 05:45:56 +01:00
兰之
8f6bf65162 fix(agents): enforce exact skill path from <available_skills> [AI-assisted] (#74161)
Summary:
- The PR updates agents skill prompt guidance to require exact `<location>` paths for single- and multi-skill selection, adds prompt assertions, and records the fix in the changelog.
- Reproducibility: yes. Static source reproduction is enough: current main lacks the exact-`<location>` guard  ... illsSection()`, while the PR diff adds it to both selection branches and asserts the resulting prompt text.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix: enforce exact skill paths for all skill matches

Validation:
- ClawSweeper review passed for head 743c9840c1.
- Required merge gates passed before the squash merge.

Prepared head SHA: 743c9840c1
Review: https://github.com/openclaw/openclaw/pull/74161#issuecomment-4341488109

Co-authored-by: tianguicheng <tianguicheng@xiaomi.com>
Co-authored-by: sallyom <somalley@redhat.com>
(cherry picked from commit c739088d62)
2026-05-05 05:45:56 +01:00
saram ali
8017dc4c3b fix(gateway): skip IPv6 loopback binding on Windows (#69701)
Bind the default loopback gateway listener only to `127.0.0.1` on Windows so libuv dual-stack `::1` behavior cannot wedge localhost HTTP requests.

Also keeps non-Windows dual-loopback behavior covered, replaces the redundant Windows passthrough test with guard coverage, and adds the required changelog entry.

Fixes #69674.

Tests:
- pnpm exec oxfmt --check --threads=1 CHANGELOG.md src/gateway/net.ts src/gateway/net.test.ts
- pnpm test src/gateway/net.test.ts
- pnpm check:changed
- GitHub required checks: green

Thanks @SARAMALI15792.

Co-authored-by: saram ali <140950904+SARAMALI15792@users.noreply.github.com>
Co-authored-by: Brad Groux <3053586+BradGroux@users.noreply.github.com>
(cherry picked from commit 978bc53e80)
2026-05-05 05:45:56 +01:00
Peter Steinberger
578d9072cf test: align beta plugin repair expectations 2026-05-05 05:40:52 +01:00
Vincent Koc
30b73bbf41 fix(plugins): honor beta channel for auto installs
(cherry picked from commit b0f841ef37)
2026-05-05 05:37:52 +01:00
Vincent Koc
ade922ba98 fix(telegram): reuse preview for long text finals (#77658)
* fix(telegram): reuse preview for long text finals

* test(qa): cover long telegram finals

* fix(qa): satisfy extension lint

* fix(qa): keep telegram long final fixture to two chunks

* test(telegram): cover three chunk finals

* fix(telegram): force long final preview boundary

(cherry picked from commit e03fe1e289)
2026-05-05 05:37:52 +01:00
Vincent Koc
997f8af734 fix(whatsapp): normalize onboarding allowlist numbers
Normalize WhatsApp onboarding allowlist entries to digit-only WhatsApp IDs and reject invalid owner-phone inputs during prompt validation.

(cherry picked from commit 68a500c465)
2026-05-05 05:37:52 +01:00
Vincent Koc
6204a6fecc fix(update): authenticate restart health probes
(cherry picked from commit b546aa91e1)
2026-05-05 05:37:25 +01:00
Peter Steinberger
9f15c29397 fix: explain missing git during plugin install
(cherry picked from commit a91c17c426)
2026-05-05 05:23:01 +01:00
Bek
cac973972c fix: slack mention-gating thread participation
(cherry picked from commit cf3ce08b91)
2026-05-05 05:14:29 +01:00
Peter Steinberger
f8f18d53fc fix: start configured generation providers
(cherry picked from commit 0eb06caae3)
2026-05-05 05:10:02 +01:00
pickaxe
696f639cf6 docs: note plugin peer-link update repair
(cherry picked from commit 712aa96a8f)
2026-05-05 05:06:31 +01:00
pickaxe
079b937b46 fix(plugins): repair missing openclaw peer links on update
(cherry picked from commit 2e8761c5c1)
2026-05-05 05:06:31 +01:00
Kelaw - Keshav's Agent
32e36d355d fix: recover missing Codex bound threads
(cherry picked from commit a373468d82)
2026-05-05 04:58:18 +01:00
Peter Steinberger
12e1c67f22 fix(build): route externalized plugin entry chunks 2026-05-05 04:31:46 +01:00
Peter Steinberger
766d02ff3b fix(build): route externalized plugin chunks 2026-05-05 04:23:24 +01:00
Peter Steinberger
e9ebb6ce6c fix(release): prune externalized plugin chunks 2026-05-05 04:15:20 +01:00
Peter Steinberger
e0002c4b5b chore(release): prepare 2026.5.4 beta 2 2026-05-05 02:42:03 +01:00
587 changed files with 21018 additions and 2138 deletions

View File

@@ -42,10 +42,12 @@ Use this skill for release and publish-time workflow. Keep ordinary development
config footprint move, so do not blindly copy stale replacement annotations
into release notes.
- Do not delete or rewrite beta tags after their matching npm package has been
published. If a pushed beta tag fails preflight before npm publish, delete and
recreate the tag and prerelease at the fixed commit so npm prerelease versions
stay contiguous. If a published beta needs a fix, commit the fix on the
release branch and increment to the next `-beta.N`.
published. If a pushed beta tag fails before npm publish, the version is not
consumed: keep the same `-beta.N`, delete/recreate or force-move the git tag
and prerelease to the fixed commit, and rerun preflight. Do not increment to
the next beta number until the matching npm package has actually published.
If a published beta needs a fix, commit the fix on the release branch and
increment to the next `-beta.N`.
- For a beta release train, run the fast local preflight first, publish the
beta to npm `beta`, then run the expensive published-package roster focused
on install/update/Docker/Parallels/NPM Telegram. If anything fails, fix it on

View File

@@ -1921,7 +1921,7 @@ jobs:
profiles: stable full
- suite_id: native-live-src-gateway-profiles-minimax
label: Native live gateway profiles MiniMax
command: OPENCLAW_LIVE_GATEWAY_PROVIDERS=minimax,minimax-portal node .release-harness/scripts/test-live-shard.mjs native-live-src-gateway-profiles
command: OPENCLAW_LIVE_GATEWAY_PROVIDERS=minimax,minimax-portal OPENCLAW_LIVE_GATEWAY_MAX_MODELS=2 node .release-harness/scripts/test-live-shard.mjs native-live-src-gateway-profiles
timeout_minutes: 90
profile_env_only: false
profiles: stable full
@@ -2223,7 +2223,7 @@ jobs:
profiles: stable full
- suite_id: live-gateway-minimax-docker
label: Docker live gateway MiniMax
command: OPENCLAW_LIVE_GATEWAY_PROVIDERS=minimax,minimax-portal OPENCLAW_LIVE_GATEWAY_MAX_MODELS=2 OPENCLAW_LIVE_GATEWAY_STEP_TIMEOUT_MS=30000 OPENCLAW_LIVE_GATEWAY_MODEL_TIMEOUT_MS=60000 OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" timeout --foreground --kill-after=30s 25m bash .release-harness/scripts/test-live-gateway-models-docker.sh
command: OPENCLAW_LIVE_GATEWAY_PROVIDERS=minimax,minimax-portal OPENCLAW_LIVE_GATEWAY_MAX_MODELS=1 OPENCLAW_LIVE_GATEWAY_STEP_TIMEOUT_MS=30000 OPENCLAW_LIVE_GATEWAY_MODEL_TIMEOUT_MS=60000 OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" timeout --foreground --kill-after=30s 25m bash .release-harness/scripts/test-live-gateway-models-docker.sh
timeout_minutes: 30
profile_env_only: false
profiles: stable full

View File

@@ -558,7 +558,7 @@ jobs:
artifact_name: ${{ needs.prepare_release_package.outputs.artifact_name }}
package_sha256: ${{ needs.prepare_release_package.outputs.package_sha256 }}
suite_profile: custom
docker_lanes: doctor-switch update-channel-switch upgrade-survivor published-upgrade-survivor plugins-offline plugin-update
docker_lanes: doctor-switch update-channel-switch update-corrupt-plugin upgrade-survivor published-upgrade-survivor plugins-offline plugin-update
published_upgrade_survivor_baselines: ${{ needs.resolve_target.outputs.run_release_soak == 'true' && 'all-since-2026.4.23' || '' }}
published_upgrade_survivor_scenarios: ${{ needs.resolve_target.outputs.run_release_soak == 'true' && 'reported-issues' || '' }}
telegram_mode: mock-openai

View File

@@ -33,10 +33,15 @@ on:
required: false
type: string
publish_openclaw_npm:
description: Publish the OpenClaw npm package after plugin npm and ClawHub publish complete
description: Publish the OpenClaw npm package after plugin npm succeeds; ClawHub may still run
required: true
default: true
type: boolean
wait_for_clawhub:
description: Wait for ClawHub plugin publish before marking this workflow complete
required: true
default: false
type: boolean
permissions:
actions: write
@@ -166,18 +171,19 @@ jobs:
PLUGIN_PUBLISH_SCOPE: ${{ inputs.plugin_publish_scope }}
PLUGINS: ${{ inputs.plugins }}
PUBLISH_OPENCLAW_NPM: ${{ inputs.publish_openclaw_npm && 'true' || 'false' }}
WAIT_FOR_CLAWHUB: ${{ inputs.wait_for_clawhub && 'true' || 'false' }}
run: |
set -euo pipefail
dispatch_and_wait() {
dispatch_workflow() {
local workflow="$1"
shift
local before_json dispatch_output run_id status conclusion url
local before_json dispatch_output run_id
before_json="$(gh run list --repo "$GITHUB_REPOSITORY" --workflow "$workflow" --event workflow_dispatch --limit 100 --json databaseId --jq '[.[].databaseId]')"
dispatch_output="$(gh workflow run --repo "$GITHUB_REPOSITORY" "$workflow" --ref "$CHILD_WORKFLOW_REF" "$@" 2>&1)"
printf '%s\n' "$dispatch_output"
printf '%s\n' "$dispatch_output" >&2
run_id="$(
printf '%s\n' "$dispatch_output" |
sed -nE 's#.*actions/runs/([0-9]+).*#\1#p' |
@@ -202,24 +208,34 @@ jobs:
exit 1
fi
echo "Dispatched ${workflow}: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${run_id}"
echo "Dispatched ${workflow}: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${run_id}" >&2
{
echo "- ${workflow}: dispatched (https://github.com/${GITHUB_REPOSITORY}/actions/runs/${run_id})"
} >> "$GITHUB_STEP_SUMMARY"
printf '%s\n' "${run_id}"
}
cancel_child() {
if [[ -n "${run_id:-}" ]]; then
echo "Cancelling child workflow ${workflow}: ${run_id}" >&2
gh run cancel --repo "$GITHUB_REPOSITORY" "$run_id" >/dev/null 2>&1 || true
fi
}
trap cancel_child EXIT INT TERM
wait_for_run() {
local workflow="$1"
local run_id="$2"
local status conclusion url updated_at last_state
last_state=""
while true; do
status="$(gh run view --repo "$GITHUB_REPOSITORY" "$run_id" --json status --jq '.status')"
run_json="$(gh run view --repo "$GITHUB_REPOSITORY" "$run_id" --json status,url,updatedAt)"
status="$(printf '%s' "$run_json" | jq -r '.status')"
if [[ "$status" == "completed" ]]; then
break
fi
url="$(printf '%s' "$run_json" | jq -r '.url')"
updated_at="$(printf '%s' "$run_json" | jq -r '.updatedAt')"
state="${status}:${updated_at}"
if [[ "$state" != "$last_state" ]]; then
echo "${workflow} still ${status} (updated ${updated_at}): ${url}"
last_state="$state"
fi
sleep 30
done
trap - EXIT INT TERM
conclusion="$(gh run view --repo "$GITHUB_REPOSITORY" "$run_id" --json conclusion --jq '.conclusion')"
url="$(gh run view --repo "$GITHUB_REPOSITORY" "$run_id" --json url --jq '.url')"
@@ -229,16 +245,41 @@ jobs:
} >> "$GITHUB_STEP_SUMMARY"
if [[ "$conclusion" != "success" ]]; then
gh run view --repo "$GITHUB_REPOSITORY" "$run_id" --json jobs --jq '.jobs[] | select(.conclusion != "success" and .conclusion != "skipped") | {name, conclusion, url}' || true
exit 1
return 1
fi
}
wait_for_run_background() {
local workflow="$1"
local run_id="$2"
local result_file="$3"
(
if wait_for_run "${workflow}" "${run_id}"; then
printf 'success\n' > "${result_file}"
else
printf 'failure\n' > "${result_file}"
fi
) &
wait_run_pid="$!"
}
{
echo "### Publish sequence"
echo
echo "- Workflow ref: \`${CHILD_WORKFLOW_REF}\`"
echo "- Release tag: \`${RELEASE_TAG}\`"
echo "- Release SHA: \`${TARGET_SHA}\`"
echo "- Plugin npm and ClawHub publish: dispatched in parallel"
if [[ "${PUBLISH_OPENCLAW_NPM}" == "true" ]]; then
echo "- OpenClaw npm publish: starts after plugin npm succeeds; ClawHub may still be running"
else
echo "- OpenClaw npm publish: skipped by input"
fi
if [[ "${WAIT_FOR_CLAWHUB}" == "true" ]]; then
echo "- Workflow completion waits for ClawHub"
else
echo "- Workflow completion does not wait for ClawHub; monitor the dispatched ClawHub run separately"
fi
} >> "$GITHUB_STEP_SUMMARY"
npm_args=(-f publish_scope="${PLUGIN_PUBLISH_SCOPE}" -f ref="${TARGET_SHA}")
@@ -248,15 +289,59 @@ jobs:
clawhub_args+=(-f plugins="${PLUGINS}")
fi
dispatch_and_wait plugin-npm-release.yml "${npm_args[@]}"
dispatch_and_wait plugin-clawhub-release.yml "${clawhub_args[@]}"
plugin_npm_run_id="$(dispatch_workflow plugin-npm-release.yml "${npm_args[@]}")"
plugin_clawhub_run_id="$(dispatch_workflow plugin-clawhub-release.yml "${clawhub_args[@]}")"
if ! wait_for_run plugin-npm-release.yml "${plugin_npm_run_id}"; then
echo "Plugin npm publish failed; cancelling ClawHub publish child ${plugin_clawhub_run_id}." >&2
gh run cancel --repo "$GITHUB_REPOSITORY" "${plugin_clawhub_run_id}" >/dev/null 2>&1 || true
exit 1
fi
openclaw_npm_run_id=""
if [[ "${PUBLISH_OPENCLAW_NPM}" == "true" ]]; then
dispatch_and_wait openclaw-npm-release.yml \
openclaw_npm_run_id="$(dispatch_workflow openclaw-npm-release.yml \
-f tag="${RELEASE_TAG}" \
-f preflight_only=false \
-f preflight_run_id="${PREFLIGHT_RUN_ID}" \
-f npm_dist_tag="${RELEASE_NPM_DIST_TAG}"
-f npm_dist_tag="${RELEASE_NPM_DIST_TAG}")"
else
echo "- OpenClaw npm publish: skipped by input" >> "$GITHUB_STEP_SUMMARY"
fi
clawhub_result=""
clawhub_pid=""
if [[ "${WAIT_FOR_CLAWHUB}" == "true" ]]; then
clawhub_result="$RUNNER_TEMP/clawhub-result.txt"
wait_run_pid=""
wait_for_run_background plugin-clawhub-release.yml "${plugin_clawhub_run_id}" "${clawhub_result}"
clawhub_pid="${wait_run_pid}"
else
echo "- plugin-clawhub-release.yml: not awaited (${plugin_clawhub_run_id})" >> "$GITHUB_STEP_SUMMARY"
fi
openclaw_result=""
openclaw_pid=""
if [[ -n "${openclaw_npm_run_id}" ]]; then
openclaw_result="$RUNNER_TEMP/openclaw-npm-result.txt"
wait_run_pid=""
wait_for_run_background openclaw-npm-release.yml "${openclaw_npm_run_id}" "${openclaw_result}"
openclaw_pid="${wait_run_pid}"
fi
failed=0
if [[ -n "${clawhub_pid}" ]] && ! wait "${clawhub_pid}"; then
failed=1
fi
if [[ -n "${openclaw_pid}" ]] && ! wait "${openclaw_pid}"; then
failed=1
fi
if [[ -f "${clawhub_result}" && "$(cat "${clawhub_result}")" != "success" ]]; then
failed=1
fi
if [[ -n "${openclaw_result}" && -f "${openclaw_result}" && "$(cat "${openclaw_result}")" != "success" ]]; then
failed=1
fi
if [[ "${failed}" != "0" ]]; then
exit 1
fi

View File

@@ -386,10 +386,10 @@ jobs:
docker_lanes="npm-onboard-channel-agent gateway-network config-reload"
;;
package)
docker_lanes="npm-onboard-channel-agent doctor-switch update-channel-switch upgrade-survivor published-upgrade-survivor plugins-offline plugin-update"
docker_lanes="npm-onboard-channel-agent doctor-switch update-channel-switch update-corrupt-plugin upgrade-survivor published-upgrade-survivor plugins-offline plugin-update"
;;
product)
docker_lanes="npm-onboard-channel-agent doctor-switch update-channel-switch upgrade-survivor published-upgrade-survivor plugins plugin-update mcp-channels cron-mcp-cleanup openai-web-search-minimal openwebui"
docker_lanes="npm-onboard-channel-agent doctor-switch update-channel-switch update-corrupt-plugin upgrade-survivor published-upgrade-survivor plugins plugin-update mcp-channels cron-mcp-cleanup openai-web-search-minimal openwebui"
include_openwebui=true
;;
full)

View File

@@ -45,6 +45,7 @@ jobs:
candidate_count: ${{ steps.plan.outputs.candidate_count }}
skipped_published_count: ${{ steps.plan.outputs.skipped_published_count }}
matrix: ${{ steps.plan.outputs.matrix }}
plan_json: ${{ steps.plan.outputs.plan_json }}
steps:
- name: Checkout
uses: actions/checkout@v6
@@ -148,12 +149,14 @@ jobs:
has_candidates="true"
fi
matrix_json="$(jq -c '.candidates' .local/plugin-clawhub-release-plan.json)"
plan_json="$(jq -c . .local/plugin-clawhub-release-plan.json)"
{
echo "candidate_count=${candidate_count}"
echo "skipped_published_count=${skipped_published_count}"
echo "has_candidates=${has_candidates}"
echo "matrix=${matrix_json}"
echo "plan_json=${plan_json}"
} >> "$GITHUB_OUTPUT"
echo "Plugin release candidates:"
@@ -182,7 +185,7 @@ jobs:
contents: read
strategy:
fail-fast: false
max-parallel: 6
max-parallel: 12
matrix:
plugin: ${{ fromJson(needs.preview_plugins_clawhub.outputs.matrix) }}
steps:
@@ -216,21 +219,26 @@ jobs:
with:
persist-credentials: false
repository: ${{ env.CLAWHUB_REPOSITORY }}
ref: main
ref: ${{ env.CLAWHUB_REF }}
path: clawhub-source
fetch-depth: 0
fetch-depth: 1
- name: Checkout pinned ClawHub CLI revision
working-directory: clawhub-source
env:
CLAWHUB_REF: ${{ env.CLAWHUB_REF }}
run: git checkout --detach "${CLAWHUB_REF}"
- name: Cache ClawHub CLI Bun artifacts
uses: actions/cache@v4
with:
path: ~/.bun/install/cache
key: clawhub-cli-bun-${{ runner.os }}-${{ env.CLAWHUB_REF }}-${{ hashFiles('clawhub-source/bun.lock', 'clawhub-source/bun.lockb') }}
restore-keys: |
clawhub-cli-bun-${{ runner.os }}-${{ env.CLAWHUB_REF }}-
- name: Install ClawHub CLI dependencies
id: clawhub_install
continue-on-error: true
working-directory: clawhub-source
run: bun install --frozen-lockfile
run: bash "$GITHUB_WORKSPACE/scripts/install-clawhub-cli-deps.sh"
- name: Bootstrap ClawHub CLI
if: steps.clawhub_install.outcome == 'success'
run: |
cat > "$RUNNER_TEMP/clawhub" <<'EOF'
#!/usr/bin/env bash
@@ -241,9 +249,15 @@ jobs:
echo "$RUNNER_TEMP" >> "$GITHUB_PATH"
- name: Verify package-local runtime build
id: runtime_build
if: steps.clawhub_install.outcome == 'success'
continue-on-error: true
run: node scripts/check-plugin-npm-runtime-builds.mjs --package "${{ matrix.plugin.packageDir }}"
- name: Preview publish command
id: preview_publish
if: steps.clawhub_install.outcome == 'success' && steps.runtime_build.outcome == 'success'
continue-on-error: true
env:
CLAWHUB_REGISTRY: ${{ env.CLAWHUB_REGISTRY }}
SOURCE_REPO: ${{ github.repository }}
@@ -253,9 +267,129 @@ jobs:
PACKAGE_DIR: ${{ matrix.plugin.packageDir }}
run: bash scripts/plugin-clawhub-publish.sh --dry-run "${PACKAGE_DIR}"
publish_plugins_clawhub:
- name: Write preview result
if: always()
env:
PLUGIN_JSON: ${{ toJson(matrix.plugin) }}
INSTALL_OUTCOME: ${{ steps.clawhub_install.outcome }}
RUNTIME_BUILD_OUTCOME: ${{ steps.runtime_build.outcome }}
PREVIEW_OUTCOME: ${{ steps.preview_publish.outcome }}
run: |
set -euo pipefail
mkdir -p .local/clawhub-preview-results
node --input-type=module <<'EOF'
import { writeFileSync } from "node:fs";
const plugin = JSON.parse(process.env.PLUGIN_JSON ?? "{}");
const outcomes = {
install: process.env.INSTALL_OUTCOME || "skipped",
runtimeBuild: process.env.RUNTIME_BUILD_OUTCOME || "skipped",
preview: process.env.PREVIEW_OUTCOME || "skipped",
};
const failed = Object.entries(outcomes).filter(([, outcome]) => outcome !== "success");
const result = {
status: failed.length === 0 ? "success" : "failure",
failedSteps: failed.map(([step, outcome]) => ({ step, outcome })),
plugin,
};
const id = String(plugin.extensionId ?? plugin.packageName ?? "plugin").replace(/[^A-Za-z0-9_.-]+/g, "-");
writeFileSync(`.local/clawhub-preview-results/${id}.json`, `${JSON.stringify(result, null, 2)}\n`);
EOF
- name: Upload preview result
if: always()
uses: actions/upload-artifact@v7
with:
name: plugin-clawhub-preview-${{ strategy.job-index }}
path: .local/clawhub-preview-results/*.json
if-no-files-found: error
- name: Fail failed preview cell
if: always() && (steps.clawhub_install.outcome != 'success' || steps.runtime_build.outcome != 'success' || steps.preview_publish.outcome != 'success')
run: exit 1
collect_preview_results:
needs: [preview_plugins_clawhub, preview_plugin_pack]
if: github.event_name == 'workflow_dispatch' && needs.preview_plugins_clawhub.outputs.has_candidates == 'true'
if: always() && needs.preview_plugins_clawhub.outputs.has_candidates == 'true'
runs-on: ubuntu-latest
permissions:
contents: read
outputs:
passed_count: ${{ steps.collect.outputs.passed_count }}
failed_count: ${{ steps.collect.outputs.failed_count }}
passed_matrix: ${{ steps.collect.outputs.passed_matrix }}
steps:
- name: Download preview results
id: download
continue-on-error: true
uses: actions/download-artifact@v8
with:
pattern: plugin-clawhub-preview-*
path: .local/clawhub-preview-results
merge-multiple: true
- name: Collect preview results
id: collect
env:
ORIGINAL_MATRIX: ${{ needs.preview_plugins_clawhub.outputs.matrix }}
run: |
set -euo pipefail
node --input-type=module <<'EOF' > .local/clawhub-preview-summary.json
import { readdirSync, readFileSync } from "node:fs";
import { join } from "node:path";
const original = JSON.parse(process.env.ORIGINAL_MATRIX || "[]");
const resultDir = ".local/clawhub-preview-results";
const results = [];
try {
for (const file of readdirSync(resultDir)) {
if (file.endsWith(".json")) {
results.push(JSON.parse(readFileSync(join(resultDir, file), "utf8")));
}
}
} catch {
// Missing artifacts are accounted for below.
}
const keyFor = (plugin) => `${plugin.packageName ?? ""}@${plugin.version ?? ""}`;
const resultByKey = new Map(results.map((result) => [keyFor(result.plugin ?? {}), result]));
const passed = [];
const failed = [];
for (const plugin of original) {
const result = resultByKey.get(keyFor(plugin));
if (result?.status === "success") {
passed.push(plugin);
} else {
failed.push({
plugin,
failedSteps: result?.failedSteps ?? [{ step: "preview-result", outcome: "missing" }],
});
}
}
console.log(JSON.stringify({ passed, failed }, null, 2));
EOF
passed_matrix="$(jq -c '.passed' .local/clawhub-preview-summary.json)"
passed_count="$(jq -r '.passed | length' .local/clawhub-preview-summary.json)"
failed_count="$(jq -r '.failed | length' .local/clawhub-preview-summary.json)"
{
echo "passed_count=${passed_count}"
echo "failed_count=${failed_count}"
echo "passed_matrix=${passed_matrix}"
} >> "$GITHUB_OUTPUT"
{
echo "### ClawHub preview results"
echo
echo "- Passed: \`${passed_count}\`"
echo "- Failed: \`${failed_count}\`"
if [[ "${failed_count}" != "0" ]]; then
echo
jq -r '.failed[] | "- \(.plugin.packageName)@\(.plugin.version): \(.failedSteps | map("\(.step)=\(.outcome)") | join(", "))"' .local/clawhub-preview-summary.json
fi
} >> "$GITHUB_STEP_SUMMARY"
publish_plugins_clawhub:
needs: [preview_plugins_clawhub, collect_preview_results]
if: always() && github.event_name == 'workflow_dispatch' && needs.preview_plugins_clawhub.outputs.has_candidates == 'true' && needs.collect_preview_results.outputs.passed_count != '0'
runs-on: ubuntu-latest
environment: clawhub-plugin-release
permissions:
@@ -263,9 +397,9 @@ jobs:
id-token: write
strategy:
fail-fast: false
max-parallel: 6
max-parallel: 12
matrix:
plugin: ${{ fromJson(needs.preview_plugins_clawhub.outputs.matrix) }}
plugin: ${{ fromJson(needs.collect_preview_results.outputs.passed_matrix) }}
steps:
- name: Checkout
uses: actions/checkout@v6
@@ -297,19 +431,21 @@ jobs:
with:
persist-credentials: false
repository: ${{ env.CLAWHUB_REPOSITORY }}
ref: main
ref: ${{ env.CLAWHUB_REF }}
path: clawhub-source
fetch-depth: 0
fetch-depth: 1
- name: Checkout pinned ClawHub CLI revision
working-directory: clawhub-source
env:
CLAWHUB_REF: ${{ env.CLAWHUB_REF }}
run: git checkout --detach "${CLAWHUB_REF}"
- name: Cache ClawHub CLI Bun artifacts
uses: actions/cache@v4
with:
path: ~/.bun/install/cache
key: clawhub-cli-bun-${{ runner.os }}-${{ env.CLAWHUB_REF }}-${{ hashFiles('clawhub-source/bun.lock', 'clawhub-source/bun.lockb') }}
restore-keys: |
clawhub-cli-bun-${{ runner.os }}-${{ env.CLAWHUB_REF }}-
- name: Install ClawHub CLI dependencies
working-directory: clawhub-source
run: bun install --frozen-lockfile
run: bash "$GITHUB_WORKSPACE/scripts/install-clawhub-cli-deps.sh"
- name: Bootstrap ClawHub CLI
run: |
@@ -392,3 +528,31 @@ jobs:
PACKAGE_TAG: ${{ matrix.plugin.publishTag }}
PACKAGE_DIR: ${{ matrix.plugin.packageDir }}
run: bash scripts/plugin-clawhub-publish.sh --publish "${PACKAGE_DIR}"
verify_plugins_clawhub:
needs: [preview_plugins_clawhub, collect_preview_results, publish_plugins_clawhub]
if: always() && needs.preview_plugins_clawhub.outputs.has_candidates == 'true'
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@v6
with:
persist-credentials: false
ref: ${{ github.ref }}
fetch-depth: 1
- name: Verify expected ClawHub versions are published
env:
CLAWHUB_REGISTRY: ${{ env.CLAWHUB_REGISTRY }}
PLAN_JSON: ${{ needs.preview_plugins_clawhub.outputs.plan_json }}
PUBLISH_RESULT: ${{ needs.publish_plugins_clawhub.result }}
run: |
set -euo pipefail
mkdir -p .local
printf '%s\n' "${PLAN_JSON}" > .local/plugin-clawhub-release-plan.json
if [[ "${PUBLISH_RESULT}" != "success" ]]; then
echo "::warning::ClawHub publish job concluded with ${PUBLISH_RESULT}; verifying registry state before failing."
fi
node scripts/plugin-clawhub-verify-published.mjs .local/plugin-clawhub-release-plan.json

View File

@@ -2,7 +2,147 @@
Docs: https://docs.openclaw.ai
## Unreleased
## 2026.5.7
### Fixes
- Release/plugin publishing: retry transient ClawHub CLI dependency install failures, keep preview-passing plugins publishable when one preview cell flakes, and verify every expected ClawHub package version after publish so maintenance releases are faster to recover and less likely to hide partial plugin publishes.
- OpenAI: support `openai/chat-latest` as an explicit direct API-key model override for trying the moving ChatGPT Instant API alias without changing the stable default model.
- Cron CLI: include computed `status` in `cron list --json` and `cron show --json` output so external tooling can read disabled/running/ok/error/skipped/idle state without reimplementing cron status derivation. (#78701) Thanks @aweiker.
- Channels CLI: make `openclaw channels list` channel-only, add `--all` for bundled and catalog channels, render installed/configured/enabled state, and move model auth/usage details to `openclaw models auth list`, `openclaw status`, and `openclaw models list`. (#78456) Thanks @sliverp.
- Native commands: honor owner enforcement for native command handlers. (#78864) Thanks @pgondhi987.
- Active Memory: require admin scope for global memory toggles. (#78863) Thanks @pgondhi987.
- Gateway/sessions: clear cached skills snapshots during `/new` and `sessions.reset` so long-lived channel sessions rebuild the visible skill list after skills change. (#78873) Thanks @Evizero.
- Auto-reply: gate inline skill tool dispatch through before-tool-call authorization hooks. (#78517) Thanks @pgondhi987.
- Tavily: resolve dedicated `tavily_search` and `tavily_extract` tool credentials from the active runtime config snapshot, so `exec` SecretRef-backed API keys do not reach the tools unresolved. (#78610) Thanks @VACInc.
- Plugins/install: use the same absolute POSIX npm lifecycle shell for managed plugin install, rollback, repair, and uninstall npm operations as staged package updates, preventing restricted PATH shells from breaking cleanup. Thanks @vincentkoc.
- Agents/context engine: invalidate cached assembled context views when source history shrinks or assembly fails, preventing stale pre-reset history from being reused. Fixes #77968. (#78163) Thanks @brokemac79 and @ChrisBot2026.
- Discord/message: parse provider-prefixed targets like `discord:channel:<id>` as channel sends instead of legacy Discord DM targets, so cross-channel agent `message(action="send")` calls no longer misroute channel IDs into misleading `Unknown Channel` failures. Fixes #78572.
- Agents/compaction: clamp compaction summary reserve tokens to each model's output limit so high-context compaction no longer requests invalid `max_tokens` values. (#54392) Thanks @adzendo.
- Commands/BTW: show the `/btw` missing-question usage placeholder with brackets so outbound channel sanitization keeps it visible. Fixes #62877. Thanks @RajvardhanPatil07.
- Cron/doctor: repair persisted cron jobs whose `payload.model` was stored as `"default"`, `"null"`, blank, or JSON `null` by removing the bad override during `openclaw doctor --fix` while keeping cron runtime model validation strict. Fixes #78549. Thanks @bizzle12368239.
- Telegram: honor `accessGroup:*` sender allowlists for DMs, groups, native commands, and callback authorization before applying Telegram's numeric sender-ID checks. Fixes #78660. Thanks @manugc.
- Agent delivery: report `deliverySucceeded=false` when outbound delivery returns no adapter result, so claimed/empty delivery paths no longer masquerade as successful sends. Fixes #78532. Thanks @joeyfrasier.
- Cron/isolated runs: fail implicit announce delivery before model execution when `delivery.channel=last` has no previous route, so recurring jobs do not spend tokens before hitting a permanent delivery-target error. Fixes #78608. Thanks @sallyom.
- Gateway/sessions: persist a new generated transcript file when daily gateway-agent session rollover changes the session id, while preserving custom transcript paths. Fixes #78607. Thanks @nailujac, @zerone0x, and @sallyom.
- Doctor/Codex OAuth: preserve working `openai-codex/*` PI routes during `doctor --fix` and recover 2026.5.5-rewritten `openai/*` GPT-5 routes when only Codex OAuth auth is available, so update repair does not break subscription-auth setups. Fixes #78407. Thanks @shakkernerd.
- Telegram: keep the polling watchdog tied to `getUpdates` liveness so unrelated outbound Bot API calls cannot mask a wedged inbound poller. Fixes #78422. Thanks @ai-hpc.
- Agents/subagents: have completed session-mode subagent registry rows honor `agents.defaults.subagents.archiveAfterMinutes` instead of a hardcoded 5-minute TTL, so registry-backed surfaces keep one retention knob across spawn modes. (#78263) Thanks @arniesaha.
- Plugins/channel setup: forward `setChannelRuntime` from non-bundled external plugin setup entries so deferred external channel runtime initializers are installed before startup polling. Fixes #77779. (#77799) Thanks @openperf.
- Telegram: treat successful same-chat `message` tool outbound sends during an inbound Telegram turn as delivered when deciding whether to emit the rewritten silent reply fallback. (#78685) Thanks @neeravmakwana.
- Gateway/tasks: reconcile stale CLI run-context tasks whose live run context disappeared and bound channel hot-reload deferrals so stale task records cannot block Discord/Slack/Telegram reloads forever.
- Discord/voice: audit Discord voice-channel permissions in `channels capabilities` and `channels status --probe`, including auto-join targets, so missing Connect/Speak/Read Message History permissions show up before `/vc join`.
- Discord/voice: make voice capture less choppy by extending the default post-speech silence grace to 2.5s, add `voice.captureSilenceGraceMs` for noisy Discord sessions, and tighten the spoken-output prompt around live STT fragments. Thanks @vincentkoc.
- WhatsApp: route proactive phone-number sends through Baileys LID forward mappings when available, so LID-addressed contacts receive agent messages instead of creating sender-only ghost chats. Fixes #67378. (#74925) Thanks @edenfunf.
- WhatsApp: send captioned `MEDIA:` directive auto-replies once instead of emitting an empty media message before the captioned media reply. (#78770) Thanks @ai-hpc.
- Codex/approvals: in Codex approval modes, stop installing the pre-guardian native `PermissionRequest` hook by default so Codex's reviewer can approve safe commands before OpenClaw surfaces an approval, remember `allow-always` decisions for identical Codex native `PermissionRequest` payloads within the active session window, and make plugin approval requests validate/render their actual allowed decisions so Telegram and other native approval UIs cannot offer stale actions. Thanks @shakkernerd.
- Model providers: normalize APNG sniffed PNG uploads, preserve Gemini 3 tool-call thought-signature replay with fallback signatures, accept legacy `__env__:VAR` custom-provider keys, and repair snake_case tool-call transcript sanitization. Fixes #51881, #48915, #77566, and #42858.
- Telegram/models: parse provider ids containing dots in `/models` callback buttons so `hf.co` model lists render as inline keyboard buttons. Fixes #38745.
## 2026.5.6
### Fixes
- Doctor/OpenAI config: keep the 2026.5.6 release branch clear of the legacy Codex route rewrite that could change OpenAI model config during `doctor --fix`, preserving existing OpenAI routes unless a supported repair path applies.
- Plugins/runtime fetch: drop third-party symbol metadata from plain request header dictionaries before passing them into native `fetch` or `Headers`, so SDK and guarded/proxy fetch paths do not reject otherwise valid plugin requests. Fixes #77846. Thanks @shakkernerd.
- Debug proxy: normalize captured fetch header dictionaries before replaying requests so symbol metadata from caller-owned header objects cannot make debug-proxy fetches fail.
- Web fetch: bound guarded dispatcher cleanup after request timeouts so timed-out fetches return tool errors instead of leaving Gateway tool lanes active. (#78439) Thanks @obviyus.
## 2026.5.5
### Fixes
- Telegram/Codex: generate DM topic labels with Codex-compatible simple-completion requests so auto-created private topics can be renamed instead of staying `New Chat`.
- Doctor/Codex OAuth: preserve working `openai-codex/*` PI routes during `doctor --fix`, recover 2026.5.5-rewritten `openai/*` GPT-5 routes when only Codex OAuth auth is available, and warn without rewriting mixed Codex OAuth plus direct OpenAI PI routes, so update repair does not break subscription-auth setups. Fixes #78407. Thanks @shakkernerd.
- Plugins/runtime fetch: drop third-party symbol metadata from plain request header dictionaries before passing them into native `fetch` or `Headers`, so SDK and guarded/proxy fetch paths do not reject otherwise valid plugin requests. Fixes #77846. Thanks @shakkernerd.
- Web fetch: bound guarded dispatcher cleanup after request timeouts so timed-out fetches return tool errors instead of leaving Gateway tool lanes active. (#78439) Thanks @obviyus.
- Mattermost/setup: prompt for and persist the server base URL after the bot token in `openclaw setup --wizard`, instead of failing validation before `--http-url` is collected. Fixes #76670. Thanks @jacobtomlinson.
- Gate Slack startup user allowlist resolution [AI]. (#77898) Thanks @pgondhi987.
- OpenAI/Codex: suppress stale `openai-codex` GPT-5.1/5.2/5.3 model refs that ChatGPT/Codex OAuth accounts now reject, keeping model lists, config validation, and forward-compat resolution on current 5.4/5.5 routes. Fixes #67158. Thanks @drpau.
- CLI/update: keep pnpm package updates on the running custom global install root and pass pnpm's `--global-dir` so `openclaw update` does not create a second default-prefix install when `OPENCLAW_HOME` or the shell points at a custom OpenClaw directory. Fixes #78377. Thanks @amknight.
- Google Meet/Voice Call: wait longer before playing PIN-derived Twilio DTMF for Meet dial-in prompts and retire stale delegated phone sessions instead of reusing completed calls.
- PDF/Codex: include extraction-fallback instructions for `openai-codex/*` PDF tool requests so Codex Responses receives its required system prompt. Fixes #77872. Thanks @anyech.
- Gateway/startup: keep the Gateway running when a configured optional plugin-owned capability such as a web_search provider or channel points at a known installable plugin that is currently unavailable; startup now logs a config warning and leaves `openclaw doctor --fix` to install or enable the plugin. (#78642) Thanks @joshavant.
- Onboard/channels: recover externalized channel plugins from stale `channels.<id>` config by falling back to `ensureChannelSetupPluginInstalled` via the trusted catalog when the plugin is missing on disk, so leftover `appId`/token entries no longer dead-end onboard with "<channel> plugin not available." (#78328) Thanks @sliverp.
- Codex/app-server: forward the OpenClaw workspace bootstrap block through Codex `developerInstructions` instead of `config.instructions`, so persona/style guidance reaches the behavior-shaping app-server lane. Fixes #77363. Thanks @lonexreb.
- MS Teams: route proactive channel sends with stored thread roots through the configured threaded reply path instead of forcing every CLI/message-tool send into a new top-level post. Fixes #78298. Thanks @amknight.
- CLI/infer: pass minimal instructions to local `openai-codex/*` model probes and surface provider error details when `infer model run` returns no text. Fixes #76464. Thanks @lilesjtu.
- Dependencies: override transitive `ip-address` to `10.2.0` so the runtime lockfile no longer includes the vulnerable `10.1.0` build flagged by Dependabot alert 109. Thanks @vincentkoc.
- Plugins/install: apply OpenClaw's npm security overrides inside managed external plugin npm roots so hoisted plugin dependencies inherit the host package hardening. Thanks @vincentkoc.
- Plugins/install: skip npm peer resolution in managed plugin roots so installing peer-based plugins such as Opik cannot pull a stale registry `openclaw` copy beside Codex/Discord/WhatsApp and trigger `ERESOLVE`. Thanks @vincentkoc.
- Plugins/uninstall: run managed npm cleanup even when a plugin package directory is already missing, preventing stale package manifests from reinstalling removed plugins on the next npm install.
- Feishu: hydrate missing native topic starter thread IDs before session routing so first turns and follow-ups stay in the same topic session. Fixes #78262. Thanks @joeyzenghuan.
- LINE: reject `dmPolicy: "open"` configs without wildcard `allowFrom` so webhook DMs fail validation instead of being acknowledged and silently blocked before inbound processing. Fixes #78316.
- Telegram/Codex: keep message-tool-only progress drafts visible and render native Codex tool progress once per tool instead of duplicating item/tool draft lines. Fixes #75641. (#77949)
- Providers/xAI: stop sending OpenAI-style reasoning effort controls to native Grok Responses models, so `xai/grok-4.3` no longer fails live Docker/Gateway runs with `Invalid reasoning effort`.
- Providers/xAI: clamp the bundled xAI thinking profile to `off` so live Gateway runs cannot send unsupported reasoning levels to native Grok Responses models.
- Matrix/approvals: retry approval delivery up to 3 times with a short backoff so transient Matrix send failures do not strand pending approval prompts. (#78179) Thanks @Patrick-Erichsen.
- Discord/gateway: measure heartbeat ACK timeouts from the actual heartbeat send, preventing late initial heartbeats from triggering false reconnect loops while the channel is still awaiting readiness. Fixes #77668. (#78087) Thanks @bryce-d-greybeard and @NikolaFC.
- Discord/guilds: route plain text control commands such as `/steer` through the normal authorization and mention gate instead of silently dropping them before an agent session can see them. Fixes #78080. Thanks @ramitrkar-hash.
- Control UI/Sessions: make the compaction count a compact `N Checkpoint(s)` disclosure and show expanded session-level details with modern checkpoint history cards across responsive table layouts. Thanks @BunsDev.
- Control UI/performance: keep chat and channel tabs responsive while history payloads and channel probes are slow, label partial channel status, and record slow chat/config render timings in the event log. Thanks @BunsDev.
- Control UI/sessions: fire the documented `/new` command and lifecycle hooks only for explicit Control UI session creation, restoring session-memory and custom hook capture without changing SDK parent-session creates. Fixes #76957. Thanks @BunsDev.
- Exec approvals: fall back to a guarded copy when Windows rejects rename-overwrite for `exec-approvals.json`, while preserving symlink, hard-link, and owner-only permission safeguards. Fixes #77785. (#77907) Thanks @Alex-Alaniz and @MilleniumGenAI.
- Slack: preserve Socket Mode SDK error context and structured Slack API fields in reconnect logs, so startup failures no longer collapse to a bare `unknown error`.
- iOS pairing: allow setup-code and manual `ws://` connects for private LAN and `.local` gateways while keeping Tailscale/public routes on `wss://`, and prefer explicit gateway passwords over stale bootstrap tokens in mixed-auth reconnects. Fixes #47887; carries forward #65185. Thanks @draix and @BunsDev.
- Plugins/diagnostics: make source-only TypeScript package warnings actionable by explaining that missing compiled runtime output is a publisher packaging issue and pointing users to update/reinstall or disable/uninstall the plugin. Fixes #77835. Thanks @googlerest.
- Control UI/chat: keep persisted assistant progress text visible when the same transcript turn also contains tool-use metadata, so chat.history reloads no longer make those replies vanish after the next user message. Fixes #77374. Thanks @BunsDev.
- Cron: repair persisted future `nextRunAtMs` values that no longer line up with the cron schedule, so daily timezone-aware jobs do not stay jumped to stale future dates. Fixes #77867. Thanks @hongfangsong.
- TUI: skip the generic CLI respawn wrapper for interactive launches, exit cleanly on terminal loss, and refuse to restore heartbeat sessions as the remembered chat session, preventing stale heartbeat history and orphaned `openclaw-tui` processes on first boot. Thanks @vincentkoc.
- Doctor/sessions: move heartbeat-poisoned default main session store entries to recovery keys and clear stale TUI restore pointers, so `doctor --fix` can repair instances already stuck on `agent:main:main` heartbeat history. Thanks @vincentkoc.
- Agents/context engines: keep hidden OpenClaw runtime-context custom messages out of context-engine assemble, afterTurn, and ingest hooks so transcript reconstruction plugins only see conversation messages. Thanks @vincentkoc.
- Gateway/shutdown: cancel delayed post-ready maintenance during close and suppress maintenance/cron startup after quick restarts, preventing orphaned background timers. Thanks @vincentkoc.
- Agents/generated media: treat attachment-style message tool actions as completed chat sends, preventing duplicate fallback media posts when generated files were already uploaded.
- Control UI/sessions: show each session's agent runtime in the Sessions table and allow filtering by runtime labels, matching the Agents panel runtime wording. Thanks @vincentkoc.
- Discord/streaming: show live reasoning text in progress drafts instead of a bare `Reasoning` status line.
- Gateway/status: avoid marking fast repeated health/status samples as event-loop degraded from CPU/utilization alone until the Gateway has accumulated a sustained sampling window. Thanks @shakkernerd.
- Plugins/update: keep installed official npm and ClawHub plugins such as Codex, Discord, WhatsApp, and diagnostics plugins synced during host updates even when disabled or previously exact-pinned, while preserving third-party plugin pins. Thanks @vincentkoc.
- Doctor/status: warn when `OPENCLAW_GATEWAY_TOKEN` would shadow a different active `gateway.auth.token` source for local CLI commands, while avoiding false positives when config points at the same env token. Fixes #74271. Thanks @yelog.
- Gateway/HTTP: avoid loading managed outgoing-image media handlers for unrelated requests, so disabled OpenAI-compatible routes return 404 without waiting on lazy media sidecars. Thanks @vincentkoc.
- Gateway/OpenAI-compatible: send the assistant role SSE chunk as soon as streaming chat-completion headers are accepted, so cold agent setup cannot leave `/v1/chat/completions` clients with a bodyless 200 response until their idle timeout fires.
- Agents/media: avoid direct generated-media completion fallback while the announce-agent run is still pending, so async video and music completions do not duplicate raw media messages. (#77754)
- WebChat/Codex media: stage Codex app-server generated local images into managed media before Gateway display, so Codex-home image paths no longer hit `LocalMediaAccessError` while keeping Codex home out of the display allowlist. Thanks @frankekn.
- TUI/sessions: bound the session picker to recent rows and use exact lookup-style refreshes for the active session, so dusty stores no longer make TUI hydrate weeks-old transcripts before becoming responsive. Thanks @vincentkoc.
- Doctor/gateway: report recent supervisor restart handoffs in `openclaw doctor --deep`, using the installed service environment when available so service-managed clean exits are visible in guided diagnostics. Thanks @shakkernerd.
- Gateway/status: show recent supervisor restart handoffs in `openclaw gateway status --deep`, including JSON details, so clean service-managed restarts are reported as restart handoffs instead of opaque stopped-service diagnostics. Thanks @shakkernerd.
- Providers/Fireworks: expose Kimi models as thinking-off-only and keep K2.5/K2.6 requests on `thinking: disabled`, so manual model switches do not send Fireworks-rejected `reasoning*` parameters. Refs #74289. Thanks @frankekn.
- WhatsApp responsiveness: stop only verified stale local TUI clients when they degrade the Gateway event loop and delay replies. Thanks @vincentkoc.
- Plugins/update: repair stale managed npm-root `openclaw` peer packages before plugin installs, so beta-channel official plugin updates are not downgraded by old core package-lock state. Thanks @vincentkoc.
- Plugins/install: reassert managed npm plugin `openclaw` peer links after shared-root npm installs, updates, and uninstalls, so mutating one plugin does not leave previously installed SDK-using plugins unable to resolve `openclaw/plugin-sdk/*`.
- Hooks/session-memory: add collision suffixes to fallback memory filenames so repeated `/new` or `/reset` captures in the same minute do not overwrite the earlier session archive. Thanks @vincentkoc.
- Agents/config: remove the ambiguous legacy `main` agent dir helper from runtime paths; model, auth, gateway, bundled plugin, and test helpers now resolve default/session agent dirs through `agents.list`/agent-scope helpers while plugin SDK keeps a deprecated compatibility export.
- CLI/status: show the selected agent runtime/harness in `openclaw status` session rows so terminal status matches the `/status` runtime line. Thanks @vincentkoc.
- CLI/sessions: prune old unreferenced transcript, compaction checkpoint, and trajectory artifacts during normal `sessions cleanup`, so gateway restart or crash orphans do not accumulate indefinitely outside `sessions.json`. Fixes #77608. Thanks @slideshow-dingo.
- Doctor/Codex: repair legacy `openai-codex/*` routes in primary models, fallbacks, heartbeat/subagent/compaction overrides, hooks, channel overrides, and stale session pins to canonical `openai/*`, selecting `agentRuntime.id: "codex"` only when the Codex plugin is installed, enabled, contributes the `codex` harness, and has usable OAuth; otherwise select `agentRuntime.id: "pi"`. Thanks @vincentkoc.
- Plugins/update: keep installed official npm and ClawHub plugins such as Codex, Discord, WhatsApp, and diagnostics plugins synced during host updates even when disabled or previously exact-pinned, while preserving third-party plugin pins. Thanks @vincentkoc.
- Video generation: accept provider-specific aspect-ratio and resolution hints at the tool boundary, normalize `720P` to MiniMax's supported `768P`, and stop sending Google `generateAudio` on Gemini video requests so provider fallback can recover from model-specific parameter differences. Thanks @vincentkoc.
- Status: show compact Gateway process uptime and host system uptime in `/status`, making restart and host-lifetime checks visible from chat. Thanks @vincentkoc.
- WhatsApp responsiveness: stop only verified stale local TUI clients when they degrade the Gateway event loop and delay replies. Thanks @vincentkoc.
- Hooks/session-memory: run reset memory capture off the command reply path and make model-generated memory filename slugs opt-in with `llmSlug: true`, so `/new` and `/reset` no longer block WhatsApp and other message-channel reset replies on hook housekeeping or a nested model call. Thanks @vincentkoc.
- CLI/gateway: pause non-TTY stdin after full CLI command completion and stop `openclaw agent` from falling back to embedded mode after gateway request/auth failures, so parent help commands exit cleanly and scoped delivery probes surface the real Gateway error immediately. Thanks @vincentkoc.
- Gateway/model catalog: cache empty read-only model catalog results until reload, so TUI and control-plane refresh loops cannot hammer plugin metadata reads when no usable models are currently discovered. Thanks @vincentkoc.
- Hooks/session-memory: add collision suffixes to fallback memory filenames so repeated `/new` or `/reset` captures in the same minute do not overwrite the earlier session archive. Thanks @vincentkoc.
- TUI/sessions: bound the session picker to recent rows and use exact lookup-style refreshes for the active session, so dusty stores no longer make TUI hydrate weeks-old transcripts before becoming responsive. Thanks @vincentkoc.
- Agents/context engines: keep hidden OpenClaw runtime-context custom messages out of context-engine assemble, afterTurn, and ingest hooks so transcript reconstruction plugins only see conversation messages. Thanks @vincentkoc.
- TUI: skip the generic CLI respawn wrapper for interactive launches, exit cleanly on terminal loss, and refuse to restore heartbeat sessions as the remembered chat session, preventing stale heartbeat history and orphaned `openclaw-tui` processes on first boot. Thanks @vincentkoc.
- Doctor/sessions: move heartbeat-poisoned default main session store entries to recovery keys and clear stale TUI restore pointers, so `doctor --fix` can repair instances already stuck on `agent:main:main` heartbeat history. Thanks @vincentkoc.
- Gateway/shutdown: report structured shutdown warnings and HTTP close timeout warnings through `ShutdownResult` while preserving lifecycle hook hardening. Carries forward #41296. Thanks @edenfunf.
- CLI/update: make dev-channel preflight lint opt-in and constrained when enabled, so `openclaw update --channel dev` no longer walks back otherwise-good main commits when Ubuntu hosts OOM-kill or fail parallel oxlint shards. Thanks @vincentkoc.
- CLI/channels: skip config, proxy, channel-option catalog, banner-config, and plugin startup bootstrap for the bare `openclaw channels` parent-help command, so it exits promptly after printing help instead of loading configured channel plugins. Thanks @vincentkoc.
- Gateway/shutdown: cancel delayed post-ready maintenance during close and suppress maintenance/cron startup after quick restarts, preventing orphaned background timers. Thanks @vincentkoc.
- CLI/status: show the selected agent runtime/harness in `openclaw status` session rows so terminal status matches the `/status` runtime line. Thanks @vincentkoc.
- Sessions CLI: show the selected agent runtime in the `openclaw sessions` table so terminal output matches the runtime visibility already present in JSON/status surfaces. Thanks @vincentkoc.
- Control UI/sessions: show each session's agent runtime in the Sessions table and allow filtering by runtime labels, matching the Agents panel runtime wording. Thanks @vincentkoc.
- Docker/Gateway: harden the gateway container by dropping `NET_RAW` and `NET_ADMIN` capabilities and enabling `no-new-privileges` in the bundled `docker-compose.yml`. Thanks @VintageAyu.
- OpenAI/Gateway: flush the initial chat stream chunk correctly so first-token streaming is visible instead of being delayed behind later chunks.
- Gateway/media: skip media sidecar handling for unrelated HTTP routes so non-media requests do not pay the media route behavior.
- Discord: show reasoning text in progress drafts so streaming replies expose useful thinking/progress instead of blank draft updates.
- Auth profiles: avoid putting providers on cooldown for format-level rejections, so fallback profiles can still be tried when a model name is unsupported.
- Update/plugins: tolerate corrupt managed plugin records during update so core package updates can still complete and report the plugin repair path.
- Update: stop dev-channel updates cleanly after a fetch failure instead of continuing into later update steps.
- Agents/generated media: treat attachment-style message tool actions as completed chat sends, preventing duplicate fallback media posts when generated files were already uploaded.
## 2026.5.4
### Highlights
@@ -10,6 +150,7 @@ Docs: https://docs.openclaw.ai
### Changes
- Gateway/Windows: bind the default loopback gateway listener only to `127.0.0.1` on Windows so libuv's dual-stack `::1` behavior cannot wedge localhost HTTP requests. (#69701, fixes #69674) Thanks @SARAMALI15792.
- Plugins/migration: emit catalog-backed install hints when `plugins.entries` or `plugins.allow` references an official external plugin that is not installed, so upgraded configs point operators to `openclaw plugins install <spec>` instead of telling them to remove valid plugin config. (#77483) Thanks @hclsys.
- OpenAI/Codex media: advertise Codex audio transcription in runtime and manifest metadata and route active Codex chat models to the OpenAI transcription default instead of sending chat model ids to audio transcription. Thanks @vincentkoc.
- Dependencies: refresh runtime and provider packages including Pi 0.73.0, ACPX adapters, OpenAI, Anthropic, Slack, and TypeScript native preview, while keeping the Bedrock runtime installer override pinned below the Windows ARM Node 24 npm resolver failure.
@@ -59,22 +200,32 @@ Docs: https://docs.openclaw.ai
- Plugins/ClawHub: annotate 429 errors from ClawHub with the reset window from `RateLimit-Reset`/`Retry-After` and append a `Sign in for higher rate limits.` hint when the request was unauthenticated, so users can see when downloads will recover and how to lift the cap. Thanks @romneyda.
- Plugins/runtime state: add `registerIfAbsent` for atomic keyed-store dedupe claims that return whether a plugin successfully claimed a key without overwriting an existing live value. Thanks @amknight.
- Plugin SDK: add plugin-owned `SessionEntry` slot projection and scoped trusted-policy session extension reads. (#75609; replaces part of #73384/#74483) Thanks @100yenadmin.
- Sandbox/Windows: accept drive-absolute Docker bind sources while keeping sandbox blocked-path and allowed-root policy comparisons Windows-case-insensitive. (#42174) Thanks @6607changchun.
### Fixes
- Plugins/install: honor the beta update channel for onboarding and doctor-managed plugin installs by requesting floating npm and ClawHub specs with `@beta` while keeping persistent install records on the catalog default. Thanks @vincentkoc.
- WhatsApp/onboarding: canonicalize setup and pairing allowlist entries to WhatsApp's digit-only phone ids while still accepting E.164, JID, and `whatsapp:` inputs, so personal-phone allowlists match WhatsApp Web sender ids after setup. Thanks @vincentkoc.
- Gateway/startup: load provider plugins that own explicitly configured image, video, or music generation defaults so generation tools become live after gateway restart instead of remaining catalog-only. Fixes #77244. Thanks @buyuangtampan, @Nikoxx99, and @vincentkoc.
- Slack/subagents: keep resumed parent `message.send` calls in the originating Slack thread when ambient session thread context is present, and suppress successful silent child completion rows from follow-up findings. Thanks @bek91.
- Slack/mentions: record thread participation for successful visible threaded Slack sends, including message-tool and media delivery paths, so unmentioned replies in bot-participated threads can bypass mention gating as documented. Fixes #77648. Thanks @bek91.
- Infra/Windows: skip the POSIX `/tmp/openclaw` preferred path on Windows in `resolvePreferredOpenClawTmpDir` so log files, TTS temp files, and other writes land in `%TEMP%\openclaw-<uid>` instead of `C:\tmp\openclaw`. Fixes #60713. Thanks @juan-flores077.
- Media/Windows: open saved attachment temp files read/write before fsync so Windows WebChat and `chat.send` media offloads no longer fail with EPERM during durability flush. (#76593) Thanks @qq230849622-a11y.
- Agents/tools: honor narrow runtime tool allowlists when constructing embedded-runner tool families and bundled MCP/LSP runtimes, so cron/subagent runs that request tools such as `update_plan`, `browser`, `x_search`, channel login tools, or `group:plugins` no longer start with missing tools or unrelated bootstrap work. (#77519, #77532)
- Codex plugin: mirror the experimental upstream app-server protocol and format generated TypeScript before drift checks, keeping OpenClaw's `experimentalApi` bridge compatible with latest Codex while preserving formatter gates.
- Telegram/media: derive no-caption inbound media placeholders from saved MIME metadata instead of the Telegram `photo` shape, so non-image and mixed attachments no longer reach the model as `<media:image>`. Fixes #69793. Thanks @aspalagin.
- Telegram/streaming: reuse the active preview as the first chunk for long text finals, so multi-chunk replies no longer create a transient extra bubble that appears and then disappears. Thanks @vincentkoc.
- Agents/cache: keep per-turn runtime context out of ordinary chat system prompts while still delivering hidden current-turn context, restoring prompt-cache reuse on chat continuations. Fixes #77431. Thanks @Udjin79.
- Gateway/startup: include resolved thinking and fast-mode defaults in the `agent model` startup log line, defaulting unset startup thinking to `medium` without mixing in reasoning visibility.
- Gateway/update: resolve local gateway probe auth from the installed config during post-update restart verification, so token/device-authenticated VPS gateways are not misreported as unhealthy port conflicts after a package swap. Thanks @vincentkoc.
- Agents/Tools: add post-compaction loop guard in `pi-embedded-runner` that arms after auto-compaction-retry and aborts the run with `compaction_loop_persisted` when the agent emits the same `(tool, args, result)` triple `windowSize` times (default 3) within that window. Disable via existing `tools.loopDetection.enabled`; tune via `tools.loopDetection.postCompactionGuard.windowSize`. Targets the failure mode where context-overflow + compaction does not break a tool-call loop. Refs #77474; carries forward #21597. Thanks @efpiva.
- Gateway/watch: suppress sync-I/O trace output during `pnpm gateway:watch --benchmark` unless explicitly requested, so CPU profiling no longer floods the terminal with stack traces.
- Gateway/watch: when benchmark sync-I/O tracing is explicitly enabled, tee trace blocks to the benchmark output log and filter them from the terminal pane while keeping normal Gateway logs visible.
- Plugins/runtime-deps: include `json5` in the memory-core plugin runtime dependency set so packaged `memory_search` sandboxes can resolve generated OpenClaw runtime chunks that parse JSON5 config. Fixes #77461.
- Plugins/Windows: show a Git install hint when npm plugin installation fails with `spawn git ENOENT`, and document the WhatsApp plugin's Git-on-PATH requirement for Baileys/libsignal installs.
- Codex harness: preserve app-server usage-limit reset details and deliver OpenClaw-owned runtime failure notices through tool-only source-reply mode, so Telegram and other chat channels tell users when Codex subscription limits or API failures block a turn instead of going silent. (#77557) Thanks @pashpashpash.
- Agents/OpenAI: default direct OpenAI Responses models to the SSE transport instead of WebSocket auto-selection, preventing pi runtime chat turns from hanging on servers where the WebSocket path stalls while the OpenAI HTTP stream works. Thanks @vincentkoc.
- Plugins/update: repair missing plugin-local `openclaw` peer links before skipping unchanged npm plugin updates, so current external Codex installs can recover `openclaw/plugin-sdk/*` resolution during OTA repair. (#77544) Thanks @ProspectOre.
- Discord/replies: treat failed final reply delivery as a failed turn instead of counting it as a delivered automatic visible reply, so guild/channel turns no longer show done when the final message was dropped. Fixes #77520. Thanks @Patrick-Erichsen.
- Discord: prefer IPv4 for Discord REST and gateway WebSocket startup paths so IPv4-only networks no longer stall before Gateway READY and inbound message dispatch. Fixes #77398; refs #77526. Thanks @Beandon13.
- Channels/plugins: key bundled package-state probes, env/config presence, and read-only command defaults by channel id instead of manifest plugin id, preserving setup and native-command detection for channel plugins whose package id differs from the channel alias. Thanks @vincentkoc.
@@ -216,6 +367,7 @@ Docs: https://docs.openclaw.ai
- Google Meet: make Twilio setup status require an enabled `voice-call` plugin entry instead of treating a missing entry as ready. Thanks @vincentkoc.
- Telegram: render shared interactive reply buttons in reply delivery so plugin approval messages show inline keyboards. (#76238) Thanks @keshavbotagent.
- Cron/sessions: keep cron metadata rows without an on-disk transcript non-resumable until a transcript exists, so doctor and `sessions cleanup --fix-missing` no longer report or prune pre-transcript cron rows as broken sessions. Refs #77011.
- OpenAI Codex: recreate missing bound app-server threads once when a stale `/codex bind` sidecar survives a restart, preserving the selected auth profile and turn overrides before retrying the inbound turn. (#76936) Thanks @keshavbotagent.
- Agents/cli-runner: drop a saved `claude-cli` resume sessionId at preparation time when its on-disk transcript no longer exists in `~/.claude/projects/`, so a stale binding from a half-installed `update.run` cannot trap follow-up runs (auto-reply / Telegram direct) in a `claude --resume` timeout loop; the run starts fresh and the new sessionId is written back through the existing post-run flow. (#77030; refs #77011) Thanks @openperf.
- Release validation: install the cross-OS TypeScript harness through Windows-safe Node/npm shims so native Windows package checks reach the OpenClaw smoke suites instead of exiting before artifact capture. Thanks @vincentkoc.
- Release validation: let Windows packaged-upgrade checks continue after the shipped 2026.5.2 updater hits its native-module swap cleanup fallback, verifying the fallback-installed candidate through package metadata and downstream smoke instead of crashing on the immediate update-status probe. Thanks @vincentkoc.
@@ -304,6 +456,7 @@ Docs: https://docs.openclaw.ai
- Tools/BTW: add `/side` as a text and native slash-command alias for `/btw` side questions.
- Doctor/config: `doctor --fix` now commits safe legacy migrations even when unrelated validation issues (e.g. a missing plugin) prevent full validation from passing, so `agents.defaults.llm` and other known-legacy keys are always cleaned up by `doctor --fix` regardless of other config problems. Fixes #76798. (#76800) Thanks @hclsys.
- Agents/tools: skip optional media and PDF tool factories when the effective tool denylist already blocks them, avoiding unnecessary hot-path setup for tools that will be filtered out before model use. (#76773) Thanks @dorukardahan.
- Agents/compaction: ignore pre-usage transcript metadata bytes when stale token snapshots estimate preflight compaction pressure, while still counting post-usage transcript tail pressure. Fixes #78604. Thanks @amknight.
- Discord/status: let explicit reaction tool calls opt into tracking subsequent tool progress on the reacted message with `trackToolCalls: true`, and use the shared tool display emoji table for status reactions.
- Gateway/config: stop Gateway startup and hot reload from auto-restoring invalid config; invalid config now fails closed and `openclaw doctor --fix` owns last-known-good repair.
- Gateway/performance: lazy-load early runtime discovery and shutdown-hook helpers, defer maintenance timers until after readiness, and trim duplicate plugin auto-enable work during Gateway startup.
@@ -324,6 +477,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- Update: repair doctor-migratable legacy config before persisting `openclaw update --channel ...`, so old Slack/Telegram streaming keys do not block switching to beta after a package update. Thanks @vincentkoc.
- Web fetch: late-bind `web_fetch` config and provider fallback metadata from the active runtime snapshot, matching `web_search` so long-lived tools do not use stale fetch provider settings. Thanks @vincentkoc.
- Plugins/discovery: demote the source-only TypeScript runtime check on already-installed `origin: "global"` plugin packages from a config-blocking error to a warning and let the runtime fall through to the TypeScript source via jiti, so a single broken installed package no longer blocks `plugins install` for unrelated plugins; install-time rejection of newly-installed source-only packages is unchanged. Thanks @romneyda.
- Providers/OpenAI Codex: stop the OAuth progress spinner before showing the manual redirect paste prompt, so callback timeouts do not spam `Browser callback did not finish` across terminals.
@@ -583,6 +737,7 @@ Docs: https://docs.openclaw.ai
- Agents/reply context: label replied-to messages as the current user message target in model-visible metadata, so short replies are grounded to their explicit reply target instead of nearby chat history. (#76817) Thanks @obviyus.
- Doctor/plugins: install configured missing official plugins such as Discord and Brave during doctor/update repair, auto-enable repaired provider plugins, preserve config when a download fails, and stop auto-enable from inventing plugin entries when no manifest declares a configured channel. Fixes #76872. Thanks @jack-stormentswe.
- Codex/app-server: stabilize transcript mirror dedupe across re-mirrored turns so reordered snapshots no longer drop reasoning entries or duplicate the assistant reply. Refs #77012. (#77046) Thanks @openperf.
- Agents/auth-profiles: do not record request-shape (`format`) rejections as auth-profile health failures, so a single per-session transcript-shape error (such as a prefill-strict 400 "conversation must end with a user message") no longer triggers a profile-wide cooldown that blocks every other healthy session sharing the same auth profile. Refs #77228. (#77280) Thanks @openperf.
## 2026.5.2
@@ -1396,6 +1551,7 @@ Docs: https://docs.openclaw.ai
- Gateway/plugins: enable the native `require()` fast path on Windows for bundled plugin modules so plugin loading uses `require()` instead of Jiti's transform pipeline, reducing startup from ~39s to ~2s on typical 6-plugin setups. Fixes #68656. (#74173) Thanks @galiniliev.
- macOS app: detect stale Gateway TLS certificate pins, automatically repair trusted Tailscale Serve rotations, and surface paired-but-disconnected Mac companion nodes so partial Gateway connections no longer look healthy. Thanks @guti.
- Feishu: recreate WebSocket clients with monitor-owned backoff only after SDK reconnect exhaustion, preserving heartbeat defaults and shutdown cleanup without treating recoverable SDK callback errors as terminal, so persistent connections recover without manual gateway restart. Fixes #52618; duplicate evidence #59753; related #55532, #68766, #72411, and #73739. Thanks @vincentkoc, @schumilin, @alex-xuweilong, @120106835, @sirfengyu, and @tianhaocui.
- Agents/skills: require exact `<location>` skill paths for both single-skill and multi-skill prompt selection, so agents do not guess or hard-code skill file paths. (#74161) Thanks @lanzhi-lee.
## 2026.4.27

View File

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

View File

@@ -1,5 +1,17 @@
# OpenClaw iOS Changelog
## 2026.5.7 - 2026-05-06
Maintenance update for the current OpenClaw release.
## 2026.5.6 - 2026-05-06
Maintenance update for the current OpenClaw release.
## 2026.5.5 - 2026-05-05
Maintenance update for the current OpenClaw development release.
## 2026.5.4 - 2026-05-04
Maintenance update for the current OpenClaw development release.

View File

@@ -2,8 +2,8 @@
// Source of truth: apps/ios/version.json
// Generated by scripts/ios-sync-versioning.ts.
OPENCLAW_IOS_VERSION = 2026.5.4
OPENCLAW_MARKETING_VERSION = 2026.5.4
OPENCLAW_IOS_VERSION = 2026.5.7
OPENCLAW_MARKETING_VERSION = 2026.5.7
OPENCLAW_BUILD_VERSION = 1
#include? "../build/Version.xcconfig"

View File

@@ -1,3 +1 @@
Maintenance update for the current OpenClaw development release.
- Gateway pairing now supports scanning QR codes from Settings and accepts full copied setup-code messages while keeping non-loopback `ws://` setup links blocked.
Maintenance update for the current OpenClaw release.

View File

@@ -1,3 +1,3 @@
{
"version": "2026.5.4"
"version": "2026.5.7"
}

View File

@@ -15,9 +15,9 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>2026.5.4</string>
<string>2026.5.7</string>
<key>CFBundleVersion</key>
<string>2026050400</string>
<string>2026050700</string>
<key>CFBundleIconFile</key>
<string>OpenClaw</string>
<key>CFBundleURLTypes</key>

View File

@@ -1812,6 +1812,7 @@ public struct SessionsCreateParams: Codable, Sendable {
public let label: String?
public let model: String?
public let parentsessionkey: String?
public let emitcommandhooks: Bool?
public let task: String?
public let message: String?
@@ -1821,6 +1822,7 @@ public struct SessionsCreateParams: Codable, Sendable {
label: String?,
model: String?,
parentsessionkey: String?,
emitcommandhooks: Bool?,
task: String?,
message: String?)
{
@@ -1829,6 +1831,7 @@ public struct SessionsCreateParams: Codable, Sendable {
self.label = label
self.model = model
self.parentsessionkey = parentsessionkey
self.emitcommandhooks = emitcommandhooks
self.task = task
self.message = message
}
@@ -1839,6 +1842,7 @@ public struct SessionsCreateParams: Codable, Sendable {
case label
case model
case parentsessionkey = "parentSessionKey"
case emitcommandhooks = "emitCommandHooks"
case task
case message
}
@@ -4673,6 +4677,7 @@ public struct PluginApprovalRequestParams: Codable, Sendable {
public let severity: String?
public let toolname: String?
public let toolcallid: String?
public let alloweddecisions: [String]?
public let agentid: String?
public let sessionkey: String?
public let turnsourcechannel: String?
@@ -4689,6 +4694,7 @@ public struct PluginApprovalRequestParams: Codable, Sendable {
severity: String?,
toolname: String?,
toolcallid: String?,
alloweddecisions: [String]?,
agentid: String?,
sessionkey: String?,
turnsourcechannel: String?,
@@ -4704,6 +4710,7 @@ public struct PluginApprovalRequestParams: Codable, Sendable {
self.severity = severity
self.toolname = toolname
self.toolcallid = toolcallid
self.alloweddecisions = alloweddecisions
self.agentid = agentid
self.sessionkey = sessionkey
self.turnsourcechannel = turnsourcechannel
@@ -4721,6 +4728,7 @@ public struct PluginApprovalRequestParams: Codable, Sendable {
case severity
case toolname = "toolName"
case toolcallid = "toolCallId"
case alloweddecisions = "allowedDecisions"
case agentid = "agentId"
case sessionkey = "sessionKey"
case turnsourcechannel = "turnSourceChannel"

View File

@@ -1812,6 +1812,7 @@ public struct SessionsCreateParams: Codable, Sendable {
public let label: String?
public let model: String?
public let parentsessionkey: String?
public let emitcommandhooks: Bool?
public let task: String?
public let message: String?
@@ -1821,6 +1822,7 @@ public struct SessionsCreateParams: Codable, Sendable {
label: String?,
model: String?,
parentsessionkey: String?,
emitcommandhooks: Bool?,
task: String?,
message: String?)
{
@@ -1829,6 +1831,7 @@ public struct SessionsCreateParams: Codable, Sendable {
self.label = label
self.model = model
self.parentsessionkey = parentsessionkey
self.emitcommandhooks = emitcommandhooks
self.task = task
self.message = message
}
@@ -1839,6 +1842,7 @@ public struct SessionsCreateParams: Codable, Sendable {
case label
case model
case parentsessionkey = "parentSessionKey"
case emitcommandhooks = "emitCommandHooks"
case task
case message
}
@@ -4673,6 +4677,7 @@ public struct PluginApprovalRequestParams: Codable, Sendable {
public let severity: String?
public let toolname: String?
public let toolcallid: String?
public let alloweddecisions: [String]?
public let agentid: String?
public let sessionkey: String?
public let turnsourcechannel: String?
@@ -4689,6 +4694,7 @@ public struct PluginApprovalRequestParams: Codable, Sendable {
severity: String?,
toolname: String?,
toolcallid: String?,
alloweddecisions: [String]?,
agentid: String?,
sessionkey: String?,
turnsourcechannel: String?,
@@ -4704,6 +4710,7 @@ public struct PluginApprovalRequestParams: Codable, Sendable {
self.severity = severity
self.toolname = toolname
self.toolcallid = toolcallid
self.alloweddecisions = alloweddecisions
self.agentid = agentid
self.sessionkey = sessionkey
self.turnsourcechannel = turnsourcechannel
@@ -4721,6 +4728,7 @@ public struct PluginApprovalRequestParams: Codable, Sendable {
case severity
case toolname = "toolName"
case toolcallid = "toolCallId"
case alloweddecisions = "allowedDecisions"
case agentid = "agentId"
case sessionkey = "sessionKey"
case turnsourcechannel = "turnSourceChannel"

View File

@@ -49,6 +49,11 @@ services:
# Let bundled local-model providers reach host-side LM Studio/Ollama via
# http://host.docker.internal:<port>. Docker Desktop usually provides this
# alias; the host-gateway mapping makes it work on Linux Docker Engine too.
cap_drop:
- NET_RAW
- NET_ADMIN
security_opt:
- no-new-privileges:true
extra_hosts:
- "host.docker.internal:host-gateway"
ports:

View File

@@ -1,4 +1,4 @@
b4491e9b8ea5606cad18c1acf06f03d35301ebec1974d201ec9ee7582d2f6001 config-baseline.json
65040b112912ccbc45e049bc6d9a877fe08f5a0daace120a3b98d8397bd9a325 config-baseline.json
9c0c9369d49c2001f91ec030e3852ccdc2ac9084229f335804aa9141c13b4795 config-baseline.core.json
cd7c0c7fb1435bc7e59099e9ac334462d5ad444016e9ab4512aae63a238f78dc config-baseline.channel.json
463c45a79d02598184caccbc6f316692df962fe6b0e84d1a3e3cc1809f862b15 config-baseline.channel.json
9832b30a696930a3da7efccf38073137571e1b66cae84e54d747b733fdafcc54 config-baseline.plugin.json

View File

@@ -1,2 +1,2 @@
43c6f668cd8301f485c64e6a663dc1b19d38c146ce2572943e2dc961973e0c6f plugin-sdk-api-baseline.json
1d877d94bebb634d90d929fe0581ba4bccf4d12d8342d179ae9bf1053e68c013 plugin-sdk-api-baseline.jsonl
05f7e1db899277adbb77ee985ef09e21fc83bfb540096d1f0e74d17863cd8a69 plugin-sdk-api-baseline.json
20b4d2401c38d6753e0b32cdfe69a63535b558283aa5a57c7b9e93b0347c9de8 plugin-sdk-api-baseline.jsonl

View File

@@ -133,6 +133,8 @@ This fires ~56 times per month instead of 01 times per month. OpenClaw use
`--model` uses the selected allowed model as that job's primary model. It is not the same as a chat-session `/model` override: configured fallback chains still apply when the job primary fails. If the requested model is not allowed or cannot be resolved, cron fails the run with an explicit validation error instead of silently falling back to the job's agent/default model selection.
If older or hand-edited `jobs.json` entries store `payload.model` as `"default"`, `"null"`, a blank string, or JSON `null`, run `openclaw doctor --fix`. Doctor removes those invalid persisted override sentinels; runtime does not support them as fallback aliases. Omit the model field to use the normal agent/default model selection.
Cron jobs can also carry payload-level `fallbacks`. When present, that list replaces the configured fallback chain for the job. Use `fallbacks: []` in the job payload/API when you want a strict cron run that tries only the selected model. If a job has `--model` but neither payload nor configured fallbacks, OpenClaw passes an explicit empty fallback override so the agent primary is not appended as a hidden extra retry target.
Model-selection precedence for isolated jobs is:

View File

@@ -178,7 +178,7 @@ openclaw hooks enable <hook-name>
### session-memory details
Extracts the last 15 user/assistant messages, generates a descriptive filename slug via LLM, and saves to `<workspace>/memory/YYYY-MM-DD-slug.md` using the host local date. Requires `workspace.dir` to be configured.
Extracts the last 15 user/assistant messages and saves to `<workspace>/memory/YYYY-MM-DD-HHMM.md` using the host local date. Memory capture runs in the background so `/new` and `/reset` acknowledgements are not delayed by transcript reads or optional slug generation. Set `hooks.internal.entries.session-memory.llmSlug: true` to generate descriptive filename slugs with the configured model. Requires `workspace.dir` to be configured.
<a id="bootstrap-extra-files"></a>

View File

@@ -151,11 +151,12 @@ Agent run completion is authoritative for active task records. A successful deta
- Cron tasks: the cron runtime no longer tracks the job as active and durable
cron run history does not show a terminal result for that run. Offline CLI
audit does not treat its own empty in-process cron runtime state as authority.
- CLI tasks: isolated child-session tasks use the child session; chat-backed
CLI tasks use the live run context instead, so lingering
channel/group/direct session rows do not keep them alive. Gateway-backed
`openclaw agent` runs also finalize from their run result, so completed runs
do not sit active until the sweeper marks them `lost`.
- CLI tasks: tasks with a run id/source id use the live run context, so
lingering child-session or chat-session rows do not keep them alive after the
gateway-owned run disappears. Legacy CLI tasks without run identity still fall
back to the child session. Gateway-backed `openclaw agent` runs also finalize
from their run result, so completed runs do not sit active until the sweeper
marks them `lost`.
## Delivery and notifications
@@ -249,7 +250,7 @@ openclaw tasks notify <lookup> state_changes
- ACP/subagent tasks check their backing child session.
- Subagent tasks whose child session has a restart-recovery tombstone are marked lost instead of being treated as recoverable backing sessions.
- Cron tasks check whether the cron runtime still owns the job, then recover terminal status from persisted cron run logs/job state before falling back to `lost`. Only the Gateway process is authoritative for the in-memory cron active-job set; offline CLI audit uses durable history but does not mark a cron task lost solely because that local Set is empty.
- Chat-backed CLI tasks check the owning live run context, not just the chat session row.
- CLI tasks with run identity check the owning live run context, not just child-session or chat-session rows.
Completion cleanup is also runtime-aware:
@@ -316,7 +317,7 @@ A sweeper runs every **60 seconds** and handles four things:
<Steps>
<Step title="Reconciliation">
Checks whether active tasks still have authoritative runtime backing. ACP/subagent tasks use child-session state, cron tasks use active-job ownership, and chat-backed CLI tasks use the owning run context. If that backing state is gone for more than 5 minutes, the task is marked `lost`.
Checks whether active tasks still have authoritative runtime backing. ACP/subagent tasks use child-session state, cron tasks use active-job ownership, and CLI tasks with run identity use the owning run context. If that backing state is gone for more than 5 minutes, the task is marked `lost`.
</Step>
<Step title="ACP session repair">
Closes terminal or orphaned parent-owned one-shot ACP sessions, and closes stale terminal or orphaned persistent ACP sessions only when no active conversation binding remains.

View File

@@ -1155,6 +1155,12 @@ Use `/vc join|leave|status` to control sessions. The command uses the account de
/vc leave
```
To inspect the bot's effective permissions before joining, run:
```bash
openclaw channels capabilities --channel discord --target channel:<voice-channel-id>
```
Auto-join example:
```json5
@@ -1197,6 +1203,8 @@ Notes:
- `@discordjs/voice` defaults are `daveEncryption=true` and `decryptionFailureTolerance=24` if unset.
- `voice.connectTimeoutMs` controls the initial `@discordjs/voice` Ready wait for `/vc join` and auto-join attempts. Default: `30000`.
- `voice.reconnectGraceMs` controls how long OpenClaw waits for a disconnected voice session to begin reconnecting before destroying it. Default: `15000`.
- Voice playback does not stop just because another user starts speaking. To avoid feedback loops, OpenClaw ignores new voice capture while TTS is playing; speak after playback finishes for the next turn.
- `voice.captureSilenceGraceMs` controls how long OpenClaw waits after Discord reports a speaker has stopped before finalizing that audio segment for STT. Default: `2500`; raise this if Discord splits normal pauses into choppy partial transcripts.
- OpenClaw also watches receive decrypt failures and auto-recovers by leaving/rejoining the voice channel after repeated failures in a short window.
- If receive logs repeatedly show `DecryptionFailed(UnencryptedWhenPassthroughDisabled)` after updating, collect a dependency report and logs. The bundled `@discordjs/voice` line includes the upstream padding fix from discord.js PR #11449, which closed discord.js issue #11419.

View File

@@ -481,9 +481,10 @@ conversion fails, OpenClaw falls back to a file attachment and logs the reason.
For `groupSessionScope: "group_topic"` and `"group_topic_sender"`, native
Feishu/Lark topic groups use the event `thread_id` (`omt_*`) as the canonical
topic session key. Normal group replies that OpenClaw turns into threads keep
using the reply root message ID (`om_*`) so the first turn and follow-up turn
stay in the same session.
topic session key. If a native topic starter event omits `thread_id`, OpenClaw
hydrates it from Feishu before routing the turn. Normal group replies that
OpenClaw turns into threads keep using the reply root message ID (`om_*`) so the
first turn and follow-up turn stay in the same session.
---

View File

@@ -68,6 +68,22 @@ Minimal config:
}
```
Public DM config:
```json5
{
channels: {
line: {
enabled: true,
channelAccessToken: "LINE_CHANNEL_ACCESS_TOKEN",
channelSecret: "LINE_CHANNEL_SECRET",
dmPolicy: "open",
allowFrom: ["*"],
},
},
}
```
Env vars (default account only):
- `LINE_CHANNEL_ACCESS_TOKEN`
@@ -119,7 +135,7 @@ openclaw pairing approve line <CODE>
Allowlists and policies:
- `channels.line.dmPolicy`: `pairing | allowlist | open | disabled`
- `channels.line.allowFrom`: allowlisted LINE user IDs for DMs
- `channels.line.allowFrom`: allowlisted LINE user IDs for DMs; `dmPolicy: "open"` requires `["*"]`
- `channels.line.groupPolicy`: `allowlist | open | disabled`
- `channels.line.groupAllowFrom`: allowlisted LINE user IDs for groups
- Per-group overrides: `channels.line.groups.<groupId>.allowFrom`

View File

@@ -344,6 +344,7 @@ curl "https://api.telegram.org/bot<bot_token>/getUpdates"
For text-only replies:
- short DM/group/topic previews: OpenClaw keeps the same preview message and performs a final edit in place, unless a visible non-preview message was sent after the preview appeared
- long text finals that split into multiple Telegram messages reuse the existing preview as the first final chunk when possible, then send only the remaining chunks
- previews followed by visible non-preview output: OpenClaw sends the completed reply as a fresh final message and cleans up the older preview, so the final answer appears after intermediate output
- previews older than about one minute: OpenClaw sends the completed reply as a fresh final message and then cleans up the preview, so Telegram's visible timestamp reflects completion time instead of the preview creation time

View File

@@ -31,12 +31,13 @@ Healthy baseline:
### WhatsApp failure signatures
| Symptom | Fastest check | Fix |
| ------------------------------- | --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| Connected but no DM replies | `openclaw pairing list whatsapp` | Approve sender or switch DM policy/allowlist. |
| Group messages ignored | Check `requireMention` + mention patterns in config | Mention the bot or relax mention policy for that group. |
| QR login times out with 408 | Check gateway `HTTPS_PROXY` / `HTTP_PROXY` env | Set a reachable proxy; use `NO_PROXY` only for bypasses. |
| Random disconnect/relogin loops | `openclaw channels status --probe` + logs | Recent reconnects are flagged even when currently connected; watch logs, restart the gateway, then relink if flapping continues. |
| Symptom | Fastest check | Fix |
| ----------------------------------- | --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| Connected but no DM replies | `openclaw pairing list whatsapp` | Approve sender or switch DM policy/allowlist. |
| Group messages ignored | Check `requireMention` + mention patterns in config | Mention the bot or relax mention policy for that group. |
| QR login times out with 408 | Check gateway `HTTPS_PROXY` / `HTTP_PROXY` env | Set a reachable proxy; use `NO_PROXY` only for bypasses. |
| Random disconnect/relogin loops | `openclaw channels status --probe` + logs | Recent reconnects are flagged even when currently connected; watch logs, restart the gateway, then relink if flapping continues. |
| Replies arrive seconds/minutes late | `openclaw doctor --fix` | Doctor stops verified stale local TUI clients when they are degrading the Gateway event loop. |
Full troubleshooting: [WhatsApp troubleshooting](/channels/whatsapp#troubleshooting)

View File

@@ -26,6 +26,16 @@ openclaw plugins install @openclaw/whatsapp
Use the bare package to follow the current official release tag. Pin an exact
version only when you need a reproducible install.
On Windows, the WhatsApp plugin needs Git on `PATH` during npm install because
one of its Baileys/libsignal dependencies is fetched from a git URL. Install
Git for Windows, then restart the shell and rerun the install:
```powershell
winget install --id Git.Git -e
```
Portable Git also works if its `bin` directory is on `PATH`.
<CardGroup cols={3}>
<Card title="Pairing" icon="link" href="/channels/pairing">
Default DM policy is pairing for unknown senders.

View File

@@ -19,13 +19,17 @@ Related docs:
```bash
openclaw channels list
openclaw channels list --all
openclaw channels status
openclaw channels capabilities
openclaw channels capabilities --channel discord --target channel:123
openclaw channels capabilities --channel discord --target channel:<voice-channel-id>
openclaw channels resolve --channel slack "#general" "@jane"
openclaw channels logs --channel all
```
`channels list` shows chat channels only: configured accounts by default, with `installed`, `configured`, and `enabled` status tags per account. Pass `--all` to also surface bundled channels that have no configured account yet and installable catalog channels that are not yet on disk. Auth providers (OAuth + API keys) and model-provider usage/quota snapshots are no longer printed here; use `openclaw models auth list` for provider auth profiles and `openclaw status` or `openclaw models list` for usage.
## Status / capabilities / resolve / logs
- `channels status`: `--probe`, `--timeout <ms>`, `--json`
@@ -108,7 +112,7 @@ openclaw channels logout --channel whatsapp
- Run `openclaw status --deep` for a broad probe.
- Use `openclaw doctor` for guided fixes.
- `openclaw channels list` prints `Claude: HTTP 403 ... user:profile` → usage snapshot needs the `user:profile` scope. Use `--no-usage`, or provide a claude.ai session key (`CLAUDE_WEB_SESSION_KEY` / `CLAUDE_WEB_COOKIE`), or re-auth via Claude CLI.
- `openclaw channels list` no longer prints model provider usage/quota snapshots. For those, use `openclaw status` (overview) or `openclaw models list` (per-provider).
- `openclaw channels status` falls back to config-only summaries when the gateway is unreachable. If a supported channel credential is configured via SecretRef but unavailable in the current command path, it reports that account as configured with degraded notes instead of showing it as not configured.
## Capabilities probe
@@ -124,7 +128,7 @@ Notes:
- `--channel` is optional; omit it to list every channel (including extensions).
- `--account` is only valid with `--channel`.
- `--target` accepts `channel:<id>` or a raw numeric channel id and only applies to Discord.
- `--target` accepts `channel:<id>` or a raw numeric channel id and only applies to Discord. For Discord voice channels, the permission check flags missing `ViewChannel`, `Connect`, `Speak`, `SendMessages`, and `ReadMessageHistory`.
- Probes are provider-specific: Discord intents + optional channel permissions; Slack bot + user scopes; Telegram bot flags + webhook; Signal daemon version; Microsoft Teams app token + Graph roles/scopes (annotated where known). Channels without probes report `Probe: unavailable`.
## Resolve names to IDs

View File

@@ -157,6 +157,8 @@ Retention and pruning are controlled in config:
<Note>
If you have cron jobs from before the current delivery and store format, run `openclaw doctor --fix`. Doctor normalizes legacy cron fields (`jobId`, `schedule.cron`, top-level delivery fields including legacy `threadId`, payload `provider` delivery aliases) and migrates simple `notify: true` webhook fallback jobs to explicit webhook delivery when `cron.webhook` is configured.
Doctor also removes persisted cron `payload.model` sentinels such as `"default"`, `"null"`, blank strings, and JSON `null`. Cron runtime still treats any non-empty `payload.model` string as an explicit model override and validates it against `agents.defaults.models`; omit the model key when a job should use the agent/default model selection.
</Note>
## Common edits
@@ -217,6 +219,8 @@ openclaw cron run <job-id> --due
openclaw cron runs --id <job-id> --limit 50
```
`cron list --json` and `cron show <job-id> --json` include a top-level `status` field on each job, computed from `enabled`, `state.runningAtMs`, and `state.lastRunStatus`. Values: `disabled`, `running`, `ok`, `error`, `skipped`, or `idle`. This mirrors the human-readable status column so external tooling can read job state without re-deriving it.
`cron runs` entries include delivery diagnostics with the intended cron target, the resolved target, message-tool sends, fallback use, and delivered state.
Agent and session retargeting:

View File

@@ -34,10 +34,11 @@ openclaw doctor --generate-gateway-token
- `--force`: apply aggressive repairs, including overwriting custom service config when needed
- `--non-interactive`: run without prompts; safe migrations and non-service repairs only
- `--generate-gateway-token`: generate and configure a gateway token
- `--deep`: scan system services for extra gateway installs
- `--deep`: scan system services for extra gateway installs and report recent Gateway supervisor restart handoffs
Notes:
- In Nix mode (`OPENCLAW_NIX_MODE=1`), read-only doctor checks still work, but `doctor --fix`, `doctor --repair`, `doctor --yes`, and `doctor --generate-gateway-token` are disabled because `openclaw.json` is immutable. Edit the Nix source for this install instead; for nix-openclaw, use the agent-first [Quick Start](https://github.com/openclaw/nix-openclaw#quick-start).
- Interactive prompts (like keychain/OAuth fixes) only run when stdin is a TTY and `--non-interactive` is **not** set. Headless runs (cron, Telegram, no terminal) will skip prompts.
- Performance: non-interactive `doctor` runs skip eager plugin loading so headless health checks stay fast. Interactive sessions still fully load plugins when a check needs their contribution.
- `--fix` (alias for `--repair`) writes a backup to `~/.openclaw/openclaw.json.bak` and drops unknown config keys, listing each removal.
@@ -45,6 +46,8 @@ Notes:
- State integrity checks now detect orphan transcript files in the sessions directory. Archiving them as `.deleted.<timestamp>` requires an interactive confirmation; `--fix`, `--yes`, and headless runs leave them in place.
- Doctor also scans `~/.openclaw/cron/jobs.json` (or `cron.store`) for legacy cron job shapes and can rewrite them in place before the scheduler has to auto-normalize them at runtime.
- On Linux, doctor warns when the user's crontab still runs legacy `~/.openclaw/bin/ensure-whatsapp.sh`; that script is no longer maintained and can log false WhatsApp gateway outages when cron lacks the systemd user-bus environment.
- When WhatsApp is enabled, doctor checks for a degraded Gateway event loop with local `openclaw-tui` clients still running. `doctor --fix` stops only verified local TUI clients so WhatsApp replies are not queued behind stale TUI refresh loops.
- Doctor checks `openai-codex/*` model refs across primary models, fallbacks, heartbeat/subagent/compaction overrides, hooks, channel model overrides, and stale session route pins. `--fix` rewrites them to `openai/*` only when the native Codex runtime is installed, enabled, contributes the `codex` harness, and has usable OAuth, or when direct OpenAI auth is already available and no usable Codex OAuth route would be moved. When `openai-codex/*` is the working Codex OAuth route through OpenClaw PI, doctor preserves it. If an earlier repair left `openai/*` GPT-5 routes on PI while only Codex OAuth auth is available, `--fix` recovers them back to `openai-codex/*`; when direct OpenAI auth is also usable, doctor warns and leaves the ambiguous mixed-auth route unchanged for explicit confirmation.
- Doctor cleans legacy plugin dependency staging state created by older OpenClaw versions. It also repairs missing downloadable plugins that are referenced by config, such as `plugins.entries`, configured channels, configured provider/search settings, or configured agent runtimes. During package updates, doctor skips package-manager plugin repair until the package swap is complete; rerun `openclaw doctor --fix` afterward if a configured plugin still needs recovery. If the download fails, doctor reports the install error and preserves the configured plugin entry for the next repair attempt.
- Doctor repairs stale plugin config by removing missing plugin ids from `plugins.allow`/`plugins.entries`, plus matching dangling channel config, heartbeat targets, and channel model overrides when plugin discovery is healthy.
- Doctor quarantines invalid plugin config by disabling the affected `plugins.entries.<id>` entry and removing its invalid `config` payload. Gateway startup already skips only that bad plugin so other plugins and channels can keep running.
@@ -65,7 +68,7 @@ Notes:
## macOS: `launchctl` env overrides
If you previously ran `launchctl setenv OPENCLAW_GATEWAY_TOKEN ...` (or `...PASSWORD`), that value overrides your config file and can cause persistent unauthorized errors.
If you previously ran `launchctl setenv OPENCLAW_GATEWAY_TOKEN ...` (or `...PASSWORD`), that value overrides your config file and can cause persistent "unauthorized" errors.
```bash
launchctl getenv OPENCLAW_GATEWAY_TOKEN

View File

@@ -282,7 +282,7 @@ Saves session context to memory when you issue `/new` or `/reset`.
openclaw hooks enable session-memory
```
**Output:** `~/.openclaw/workspace/memory/YYYY-MM-DD-slug.md`
**Output:** `~/.openclaw/workspace/memory/YYYY-MM-DD-HHMM.md` by default. Set `hooks.internal.entries.session-memory.llmSlug: true` for model-generated filename slugs.
**See:** [session-memory documentation](/automation/hooks#session-memory)

View File

@@ -130,7 +130,7 @@ is available, then fall back to `latest`.
<Accordion title="Hook packs and npm specs">
`plugins install` is also the install surface for hook packs that expose `openclaw.hooks` in `package.json`. Use `openclaw hooks` for filtered hook visibility and per-hook enablement, not package installation.
Npm specs are **registry-only** (package name + optional **exact version** or **dist-tag**). Git/URL/file specs and semver ranges are rejected. Dependency installs run project-local with `--ignore-scripts` for safety, even when your shell has global npm install settings.
Npm specs are **registry-only** (package name + optional **exact version** or **dist-tag**). Git/URL/file specs and semver ranges are rejected. Dependency installs run project-local with `--ignore-scripts` for safety, even when your shell has global npm install settings. Managed plugin npm roots inherit OpenClaw's package-level npm `overrides`, so host security pins apply to hoisted plugin dependencies too.
Use `npm:<package>` when you want to make npm resolution explicit. Bare package specs also install directly from npm during the launch cutover.

View File

@@ -26,6 +26,7 @@ Notes:
- Session status output separates `Execution:` from `Runtime:`. `Execution` is the sandbox path (`direct`, `docker/*`), while `Runtime` tells you whether the session is using `OpenClaw Pi Default`, `OpenAI Codex`, a CLI backend, or an ACP backend such as `codex (acp/acpx)`. See [Agent runtimes](/concepts/agent-runtimes) for the provider/model/runtime distinction.
- MiniMax's raw `usage_percent` / `usagePercent` fields are remaining quota, so OpenClaw inverts them before display; count-based fields win when present. `model_remains` responses prefer the chat-model entry, derive the window label from timestamps when needed, and include the model name in the plan label.
- When the current session snapshot is sparse, `/status` can backfill token and cache counters from the most recent transcript usage log. Existing nonzero live values still win over transcript fallback values.
- `/status` includes compact Gateway process uptime and host system uptime.
- Transcript fallback can also recover the active runtime model label when the live session entry is missing it. If that transcript model differs from the selected model, status resolves the context window against the recovered runtime model instead of the selected one.
- For prompt-size accounting, transcript fallback prefers the larger prompt-oriented total when session metadata is missing or smaller, so custom-provider sessions do not collapse to `0` token displays.
- Output includes per-agent session stores when multiple agents are configured.

View File

@@ -87,7 +87,9 @@ Previews or applies task and Task Flow reconciliation, cleanup stamping, and pru
For cron tasks, reconciliation uses persisted run logs/job state before marking an
old active task `lost`, so completed cron runs do not become false audit errors
just because the in-memory Gateway runtime state is gone. Offline CLI audit is
not authoritative for the Gateway's process-local cron active-job set.
not authoritative for the Gateway's process-local cron active-job set. CLI tasks
with a run id/source id are marked `lost` when their live Gateway run context is
gone, even if an old child-session row remains.
### `flow`

View File

@@ -38,8 +38,9 @@ openclaw --update
- `--tag <dist-tag|version|spec>`: override the package target for this update only. For package installs, `main` maps to `github:openclaw/openclaw#main`.
- `--dry-run`: preview planned update actions (channel/tag/target/restart flow) without writing config, installing, syncing plugins, or restarting.
- `--json`: print machine-readable `UpdateRunResult` JSON, including
`postUpdate.plugins.integrityDrifts` when npm plugin artifact drift is
detected during post-update plugin sync.
`postUpdate.plugins.warnings` when corrupt or unloadable managed plugins need
repair after the core update succeeds, and `postUpdate.plugins.integrityDrifts`
when npm plugin artifact drift is detected during post-update plugin sync.
- `--timeout <seconds>`: per-step timeout (default is 1800s).
- `--yes`: skip confirmation prompts (for example downgrade confirmation).
@@ -147,7 +148,7 @@ manually.
Dev only.
</Step>
<Step title="Preflight build (dev only)">
Runs lint and TypeScript build in a temp worktree. If the tip fails, walks back up to 10 commits to find the newest clean build.
Runs the TypeScript build in a temp worktree. If the tip fails, walks back up to 10 commits to find the newest buildable commit. Set `OPENCLAW_UPDATE_PREFLIGHT_LINT=1` to also run lint during this preflight; lint runs in constrained serial mode because user update hosts are often smaller than CI runners.
</Step>
<Step title="Rebase">
Rebases onto the selected commit (dev only).
@@ -177,7 +178,7 @@ If an exact pinned npm plugin update resolves to an artifact whose integrity dif
</Warning>
<Note>
Post-update plugin sync failures fail the update result and stop restart follow-up work. Fix the plugin install or update error, then rerun `openclaw update`.
Post-update plugin sync failures that are scoped to a managed plugin are reported as warnings after the core update succeeds. The JSON result keeps the top-level update `status: "ok"` and reports `postUpdate.plugins.status: "warning"` with `openclaw doctor --fix` and `openclaw plugins inspect <id> --runtime --json` guidance. Unexpected updater or sync exceptions still fail the update result. Fix the plugin install or update error, then rerun `openclaw doctor --fix` or `openclaw update`.
When the updated Gateway starts, plugin loading is verify-only: startup does not run package managers or mutate dependency trees. Package-manager `update.run` restarts bypass the normal idle deferral and restart cooldown after the package tree has been swapped, so the old process cannot keep lazy-loading removed chunks.

View File

@@ -232,6 +232,8 @@ Scenarios (`extensions/qa-lab/src/live-transports/telegram/telegram-live.runtime
- `telegram-tools-compact-command`
- `telegram-whoami-command`
- `telegram-context-command`
- `telegram-long-final-reuses-preview`
- `telegram-long-final-three-chunks`
Output artifacts:

View File

@@ -545,7 +545,7 @@ See [Multiple Gateways](/gateway/multiple-gateways).
- `"hot"`: apply changes in-process without restarting.
- `"hybrid"` (default): try hot reload first; fall back to restart if required.
- `debounceMs`: debounce window in ms before config changes are applied (non-negative integer).
- `deferralTimeoutMs`: optional maximum time in ms to wait for in-flight operations before forcing a restart. Omit it to use the default bounded wait (`300000`); set `0` to wait indefinitely and log periodic still-pending warnings.
- `deferralTimeoutMs`: optional maximum time in ms to wait for in-flight operations before forcing a restart or channel hot reload. Omit it to use the default bounded wait (`300000`); set `0` to wait indefinitely and log periodic still-pending warnings.
---

View File

@@ -107,6 +107,8 @@ cat ~/.openclaw/openclaw.json
- Matrix channel legacy state migration (in `--fix` / `--repair` mode).
- Gateway runtime checks (service installed but not running; cached launchd label).
- Channel status warnings (probed from the running gateway).
- WhatsApp responsiveness checks for degraded Gateway event-loop health with local TUI clients still running; `--fix` stops only verified local TUI clients.
- Codex route repair for `openai-codex/*` model refs in primary models, fallbacks, heartbeat/subagent/compaction overrides, hooks, channel model overrides, and session route pins; `--fix` preserves working Codex OAuth PI routes, rewrites to `openai/*` only when the native Codex runtime or direct OpenAI auth path is usable, recovers prior `openai/*` GPT-5 PI rewrites when only Codex OAuth auth is available, and warns without rewriting when both Codex OAuth and direct OpenAI auth make an already-rewritten PI route ambiguous.
- Supervisor config audit (launchd/systemd/schtasks) with optional repair.
- Embedded proxy environment cleanup for gateway services that captured shell `HTTP_PROXY` / `HTTPS_PROXY` / `NO_PROXY` values during install or update.
- Gateway runtime best-practice checks (Node vs Bun, version-manager paths).
@@ -164,7 +166,7 @@ That stages grounded durable candidates into the short-term dreaming store while
<Accordion title="1. Config normalization">
If the config contains legacy value shapes (for example `messages.ackReaction` without a channel-specific override), doctor normalizes them into the current schema.
That includes legacy Talk flat fields. Current public Talk config is `talk.provider` + `talk.providers.<provider>`. Doctor rewrites old `talk.voiceId` / `talk.voiceAliases` / `talk.modelId` / `talk.outputFormat` / `talk.apiKey` shapes into the provider map.
That includes legacy Talk flat fields. Current public Talk speech config is `talk.provider` + `talk.providers.<provider>`, and realtime voice config is `talk.realtime.*`. Doctor rewrites old `talk.voiceId` / `talk.voiceAliases` / `talk.modelId` / `talk.outputFormat` / `talk.apiKey` shapes into the provider map, and rewrites legacy top-level realtime selectors (`talk.mode`, `talk.transport`, `talk.brain`, `talk.model`, `talk.voice`) into `talk.realtime`.
Doctor also warns when `plugins.allow` is non-empty and tool policy uses
wildcard or plugin-owned tool entries. `tools.allow: ["*"]` only matches tools
@@ -183,7 +185,7 @@ That stages grounded durable candidates into the short-term dreaming store while
- Show the migration it applied.
- Rewrite `~/.openclaw/openclaw.json` with the updated schema.
The Gateway also auto-runs doctor migrations on startup when it detects a legacy config format, so stale configs are repaired without manual intervention. Cron job store migrations are handled by `openclaw doctor --fix`.
Gateway startup refuses legacy config formats and asks you to run `openclaw doctor --fix`; it does not rewrite `openclaw.json` on startup. Cron job store migrations are also handled by `openclaw doctor --fix`.
Current migrations:
@@ -197,6 +199,7 @@ That stages grounded durable candidates into the short-term dreaming store while
- `routing.bindings` → top-level `bindings`
- `routing.agents`/`routing.defaultAgentId` → `agents.list` + `agents.list[].default`
- legacy `talk.voiceId`/`talk.voiceAliases`/`talk.modelId`/`talk.outputFormat`/`talk.apiKey` → `talk.provider` + `talk.providers.<provider>`
- legacy top-level realtime Talk selectors (`talk.mode`/`talk.transport`/`talk.brain`/`talk.model`/`talk.voice`) + `talk.provider`/`talk.providers` → `talk.realtime`
- `routing.agentToAgent` → `tools.agentToAgent`
- `routing.transcribeAudio` → `tools.media.audio.models`
- `messages.tts.<provider>` (`openai`/`elevenlabs`/`microsoft`/`edge`) → `messages.tts.providers.<provider>`
@@ -259,21 +262,24 @@ That stages grounded durable candidates into the short-term dreaming store while
<Accordion title="2e. Codex OAuth provider overrides">
If you previously added legacy OpenAI transport settings under `models.providers.openai-codex`, they can shadow the built-in Codex OAuth provider path that newer releases use automatically. Doctor warns when it sees those old transport settings alongside Codex OAuth so you can remove or rewrite the stale transport override and get the built-in routing/fallback behavior back. Custom proxies and header-only overrides are still supported and do not trigger this warning.
</Accordion>
<Accordion title="2f. Codex plugin route warnings">
When the bundled Codex plugin is enabled, doctor also checks whether `openai-codex/*` primary model refs still resolve through the default PI runner. That combination is valid when you want Codex OAuth/subscription auth through PI, but it is easy to confuse with the native Codex app-server harness. Doctor warns and points to the explicit app-server shape: `openai/*` plus `agentRuntime.id: "codex"` or `OPENCLAW_AGENT_RUNTIME=codex`.
<Accordion title="2f. Codex route repair">
Doctor checks for `openai-codex/*` model refs. Those refs are valid when you intentionally want Codex OAuth through OpenClaw PI. Native Codex harness routing uses canonical `openai/*` model refs plus `agentRuntime.id: "codex"` so the turn goes through the Codex app-server harness instead of the OpenClaw PI OpenAI path.
Doctor does not repair this automatically because both routes are valid:
In `--fix` / `--repair` mode, doctor evaluates affected default-agent and per-agent refs, including primary models, fallbacks, heartbeat/subagent/compaction overrides, hooks, channel model overrides, and stale persisted session route state:
- `openai-codex/*` + PI means "use Codex OAuth/subscription auth through the normal OpenClaw runner."
- `openai/*` + `agentRuntime.id: "codex"` means "run the embedded turn through native Codex app-server."
- Working `openai-codex/*` Codex OAuth PI routes are preserved.
- `openai-codex/*` becomes `openai/*` with `agentRuntime.id: "codex"` only when Codex is installed, enabled, contributes the `codex` harness, and has usable OAuth.
- `openai-codex/*` becomes `openai/*` on PI only when direct OpenAI auth is already usable and no working Codex OAuth route would be moved.
- Prior `openai/*` GPT-5 PI rewrites are recovered back to supported `openai-codex/*` refs when only Codex OAuth auth is available.
- Prior `openai/*` GPT-5 PI rewrites warn and stay unchanged when direct OpenAI auth is also usable, so users can confirm whether the direct OpenAI API route is intentional before switching back to Codex OAuth through PI.
- Existing model fallback lists are preserved when refs are rewritten or recovered; copied per-model settings move with the selected key.
- Persisted session `modelProvider`/`providerOverride`, `model`/`modelOverride`, fallback notices, auth-profile pins, and Codex harness pins are repaired across all discovered agent session stores when the route can be moved safely.
- `/codex ...` means "control or bind a native Codex conversation from chat."
- `/acp ...` or `runtime: "acp"` means "use the external ACP/acpx adapter."
If the warning appears, choose the route you intended and edit config manually. Keep the warning as-is when PI Codex OAuth is intentional.
</Accordion>
<Accordion title="2g. Session route cleanup">
Doctor also scans the active sessions store for stale auto-created route state after you move the configured default/fallback model or runtime away from a plugin-owned route such as Codex.
Doctor also scans discovered agent session stores for stale auto-created route state after you move configured models or runtime away from a plugin-owned route such as Codex.
`openclaw doctor --fix` can clear auto-created stale state such as `modelOverrideSource: "auto"` model pins, runtime model metadata, pinned harness ids, CLI session bindings, and auto auth-profile overrides when their owning route is no longer configured. Explicit user or legacy session model choices are reported for manual review and left untouched; switch them with `/model ...`, `/new`, or reset the session when that route is no longer intended.

View File

@@ -167,7 +167,7 @@ targets the shipped npm package instead.
Release checks call Package Acceptance with the package/update/plugin set:
```text
doctor-switch update-channel-switch upgrade-survivor published-upgrade-survivor plugins-offline plugin-update
doctor-switch update-channel-switch update-corrupt-plugin upgrade-survivor published-upgrade-survivor plugins-offline plugin-update
```
They also pass:
@@ -178,9 +178,10 @@ published_upgrade_survivor_scenarios=reported-issues
telegram_mode=mock-openai
```
This keeps package migration, update channel switching, stale plugin dependency
cleanup, offline plugin coverage, plugin update behavior, and Telegram package
QA on the same resolved artifact.
This keeps package migration, update channel switching, corrupt managed-plugin
tolerance, stale plugin dependency cleanup, offline plugin coverage, plugin
update behavior, and Telegram package QA on the same resolved artifact without
making the default release package gate walk every published release.
`all-since-2026.4.23` is the Full Release CI upgrade sample: every stable npm-published release from `2026.4.23` through `latest`. For exhaustive published
update migration coverage, use `all-since-2026.4.23` in the separate Update

View File

@@ -332,7 +332,7 @@ See [ClawDock](/install/clawdock) for the full helper guide.
`openclaw-cli` uses `network_mode: "service:openclaw-gateway"` so CLI
commands can reach the gateway over `127.0.0.1`. Treat this as a shared
trust boundary. The compose config drops `NET_RAW`/`NET_ADMIN` and enables
`no-new-privileges` on `openclaw-cli`.
`no-new-privileges` on both `openclaw-gateway` and `openclaw-cli`.
</Accordion>
<Accordion title="Permissions and EACCES">

View File

@@ -87,9 +87,12 @@ If your config uses `plugins.allow`, include `codex` there too:
}
```
Do not use `openai-codex/gpt-*` when you mean native Codex runtime. That prefix
is the explicit "Codex OAuth through PI" route. Config changes apply to new or
reset sessions; existing sessions keep their recorded runtime.
Use `openai-codex/gpt-*` only when you intentionally want Codex OAuth through
OpenClaw PI. `openclaw doctor --fix` preserves that working subscription route,
rewrites it to `openai/gpt-*` only when the native Codex runtime or direct
OpenAI auth path is usable, can recover prior `openai/gpt-*` PI rewrites back
to supported `openai-codex/gpt-*` refs when only Codex OAuth auth is available,
and warns without rewriting when direct OpenAI auth makes the route ambiguous.
## What this plugin changes
@@ -106,7 +109,9 @@ The bundled `codex` plugin contributes several separate capabilities:
Enabling the plugin makes those capabilities available. It does **not**:
- start using Codex for every OpenAI model
- convert `openai-codex/*` model refs into the native runtime
- convert a working `openai-codex/*` PI route into the native runtime without
doctor verifying that Codex is installed, enabled, contributes the `codex`
harness, and is OAuth-ready
- make ACP/acpx the default Codex path
- hot-switch existing sessions that already recorded a PI runtime
- replace OpenClaw channel delivery, session files, auth-profile storage, or
@@ -145,10 +150,13 @@ want native app-server execution. Legacy `codex/*` model refs still auto-select
the harness for compatibility, but runtime-backed legacy provider prefixes are
not shown as normal model/provider choices.
If the `codex` plugin is enabled but the primary model is still
`openai-codex/*`, `openclaw doctor` warns instead of changing the route. That is
intentional: `openai-codex/*` remains the PI Codex OAuth/subscription path, and
native app-server execution stays an explicit runtime choice.
If any configured model route is `openai-codex/*`, `openclaw doctor --fix`
preserves it when it is the working Codex OAuth PI route. For matching agent
routes, doctor rewrites it to `openai/*` and sets the agent runtime to `codex`
only when the Codex plugin is installed, enabled, contributes the `codex`
harness, and has usable OAuth. Doctor rewrites to direct `openai/*` on PI only
when direct OpenAI auth is already usable and no working Codex OAuth route would
be moved.
## Route map
@@ -158,15 +166,18 @@ Use this table before changing config:
| ---------------------------------------------------- | -------------------------- | -------------------------------------- | ---------------------------- | ------------------------------ |
| ChatGPT/Codex subscription with native Codex runtime | `openai/gpt-*` | `agentRuntime.id: "codex"` | Codex OAuth or Codex account | `Runtime: OpenAI Codex` |
| OpenAI API through normal OpenClaw runner | `openai/gpt-*` | omitted or `runtime: "pi"` | OpenAI API key | `Runtime: OpenClaw Pi Default` |
| ChatGPT/Codex subscription through PI | `openai-codex/gpt-*` | omitted or `runtime: "pi"` | OpenAI Codex OAuth provider | `Runtime: OpenClaw Pi Default` |
| Codex subscription through PI | `openai-codex/gpt-*` | omitted or `runtime: "pi"` | Codex OAuth | `Runtime: OpenClaw Pi Default` |
| Mixed providers with conservative auto mode | provider-specific refs | `agentRuntime.id: "auto"` | Per selected provider | Depends on selected runtime |
| Explicit Codex ACP adapter session | ACP prompt/model dependent | `sessions_spawn` with `runtime: "acp"` | ACP backend auth | ACP task/session status |
The important split is provider versus runtime:
- `openai-codex/*` answers "which provider/auth route should PI use?"
- `agentRuntime.id: "codex"` answers "which loop should execute this
embedded turn?"
- `openai-codex/*` is the explicit Codex OAuth through PI route; doctor preserves it when that route is working.
- `agentRuntime.id: "codex"` requires the Codex harness and fails closed if it
is unavailable.
- `agentRuntime.id: "auto"` lets registered harnesses claim matching provider
routes, but canonical OpenAI refs are still PI-owned unless a harness supports
that provider/model pair.
- `/codex ...` answers "which native Codex conversation should this chat bind
or control?"
- ACP answers "which external harness process should acpx launch?"
@@ -175,33 +186,29 @@ The important split is provider versus runtime:
OpenAI-family routes are prefix-specific. For the common subscription plus
native Codex runtime setup, use `openai/*` with `agentRuntime.id: "codex"`.
Use `openai-codex/*` only when you intentionally want Codex OAuth through PI:
For Codex OAuth through OpenClaw PI, use `openai-codex/*`:
| Model ref | Runtime path | Use when |
| --------------------------------------------- | -------------------------------------------- | ------------------------------------------------------------------------- |
| `openai/gpt-5.4` | OpenAI provider through OpenClaw/PI plumbing | You want current direct OpenAI Platform API access with `OPENAI_API_KEY`. |
| `openai-codex/gpt-5.5` | OpenAI Codex OAuth through OpenClaw/PI | You want ChatGPT/Codex subscription auth with the default PI runner. |
| `openai-codex/gpt-5.5` | Codex OAuth through OpenClaw PI | You intentionally want subscription auth on the normal PI runner. |
| `openai/gpt-5.5` + `agentRuntime.id: "codex"` | Codex app-server harness | You want ChatGPT/Codex subscription auth with native Codex execution. |
GPT-5.5 can appear on both direct OpenAI API-key and Codex subscription routes
when your account exposes them. Use `openai/gpt-5.5` with the Codex app-server
harness for native Codex runtime, `openai-codex/gpt-5.5` for PI OAuth, or
`openai/gpt-5.5` without a Codex runtime override for direct API-key traffic.
harness for native Codex runtime, or `openai/gpt-5.5` without a Codex runtime
override for direct API-key traffic.
Legacy `codex/gpt-*` refs remain accepted as compatibility aliases. Doctor
compatibility migration rewrites legacy primary runtime refs to canonical model
refs and records the runtime policy separately, while fallback-only legacy refs
are left unchanged because runtime is configured for the whole agent container.
New PI Codex OAuth configs should use `openai-codex/gpt-*`; new native
app-server harness configs should use `openai/gpt-*` plus
`agentRuntime.id: "codex"`.
compatibility migration rewrites legacy runtime refs to canonical model refs
and records the runtime policy separately. New native app-server harness configs
should use `openai/gpt-*` plus `agentRuntime.id: "codex"`.
`agents.defaults.imageModel` follows the same prefix split. Use
`openai-codex/gpt-*` when image understanding should run through the OpenAI
Codex OAuth provider path. Use `codex/gpt-*` when image understanding should run
through a bounded Codex app-server turn. The Codex app-server model must
advertise image input support; text-only Codex models fail before the media turn
starts.
`openai/gpt-*` for the normal OpenAI route and `codex/gpt-*` when image
understanding should run through a bounded Codex app-server turn. The Codex
app-server model must advertise image input support; text-only Codex models
fail before the media turn starts.
Use `/status` to confirm the effective harness for the current session. If the
selection is surprising, enable debug logging for the `agents/harness` subsystem
@@ -211,22 +218,22 @@ in `auto` mode, each plugin candidate's support result.
### What doctor warnings mean
`openclaw doctor` warns when all of these are true:
`openclaw doctor` warns when configured model refs or persisted session route
state still use `openai-codex/*` so you can confirm whether PI subscription auth
is intentional. `openclaw doctor --fix` uses the available auth/runtime state to
choose one of these outcomes:
- the bundled `codex` plugin is enabled or allowed
- an agent's primary model is `openai-codex/*`
- that agent's effective runtime is not `codex`
- preserve `openai-codex/*` when it is a working Codex OAuth PI route
- rewrite to `openai/<model>` plus `agentRuntime.id: "codex"` when Codex is installed, enabled, contributes the `codex` harness, and has usable OAuth
- rewrite to direct `openai/<model>` on PI only when direct OpenAI auth is already usable and no working Codex OAuth route would be moved
- recover prior `openai/*` GPT-5 PI rewrites back to supported `openai-codex/*` refs when only Codex OAuth auth is available
- warn without rewriting when a prior `openai/*` GPT-5 PI rewrite has both Codex OAuth and direct OpenAI auth available
That warning exists because users often expect "Codex plugin enabled" to imply
"native Codex app-server runtime." OpenClaw does not make that leap. The warning
means:
- **No change is required** if you intended ChatGPT/Codex OAuth through PI.
- Change the model to `openai/<model>` and set
`agentRuntime.id: "codex"` if you intended native app-server
execution.
- Existing sessions still need `/new` or `/reset` after a runtime change,
because session runtime pins are sticky.
The `codex` route forces the native Codex harness. The `pi` route keeps the
agent on the default OpenClaw runner instead of enabling or installing Codex as
a side effect of legacy-route cleanup.
Doctor also repairs stale persisted session pins across discovered agent session
stores so old conversations do not stay wedged on the removed route.
Harness selection is not a live session control. When an embedded turn runs,
OpenClaw records the selected harness id on that session and keeps using it for
@@ -273,9 +280,9 @@ filenames for persona files, because Codex fallbacks only apply when
For OpenClaw workspace parity, the Codex harness resolves the other bootstrap
files (`SOUL.md`, `TOOLS.md`, `IDENTITY.md`, `USER.md`, `HEARTBEAT.md`,
`BOOTSTRAP.md`, and `MEMORY.md` when present) and forwards them through Codex
config instructions on `thread/start` and `thread/resume`. This keeps
`SOUL.md` and related workspace persona/profile context visible without
duplicating `AGENTS.md`.
developer instructions on `thread/start` and `thread/resume`. This keeps
`SOUL.md` and related workspace persona/profile context visible on the native
Codex behavior-shaping lane without duplicating `AGENTS.md`.
## Add Codex alongside other models
@@ -349,7 +356,7 @@ Agents should route user requests by intent, not by the word "Codex" alone:
| "File a support report for a bad Codex run" | `/diagnostics [note]` |
| "Only send Codex feedback for this attached thread" | `/codex diagnostics [note]` |
| "Use my ChatGPT/Codex subscription with Codex runtime" | `openai/*` plus `agentRuntime.id: "codex"` |
| "Use my ChatGPT/Codex subscription through PI" | `openai-codex/*` model refs |
| "Check or recover Codex OAuth PI routes" | `openclaw doctor --fix` |
| "Run Codex through ACP/acpx" | ACP `sessions_spawn({ runtime: "acp", ... })` |
| "Start Claude Code/Gemini/OpenCode/Cursor in a thread" | ACP/acpx, not `/codex` and not native sub-agents |

View File

@@ -43,7 +43,7 @@ OpenClaw uses stable per-source roots:
npm installs run in the npm root with:
```bash
npm install --prefix ~/.openclaw/npm <spec> --omit=dev --ignore-scripts --no-audit --no-fund
npm install --prefix ~/.openclaw/npm <spec> --omit=dev --omit=peer --legacy-peer-deps --ignore-scripts --no-audit --no-fund
```
npm may hoist transitive dependencies to `~/.openclaw/npm/node_modules` beside
@@ -51,6 +51,14 @@ the plugin package. OpenClaw scans the managed npm root before trusting the
install and uses npm to remove npm-managed packages during uninstall, so hoisted
runtime dependencies stay inside the managed cleanup boundary.
Plugins that import `openclaw/plugin-sdk/*` declare `openclaw` as a peer
dependency. OpenClaw does not let npm install a separate registry copy of the
host package into the managed root, because stale host packages can affect npm
peer resolution during later plugin installs. Managed npm installs skip npm peer
resolution/materialization for the shared root and OpenClaw reasserts
plugin-local `node_modules/openclaw` links for installed packages that declare
the host peer after install, update, or uninstall.
git installs clone or refresh the repository, then run:
```bash

View File

@@ -18,6 +18,16 @@ Adds the WhatsApp channel surface for sending and receiving OpenClaw messages.
channels: whatsapp
## Windows install note
On Windows, the WhatsApp plugin needs Git on `PATH` during npm install because one of its Baileys/libsignal dependencies is fetched from a git URL. Install Git for Windows, then restart the shell and rerun the install:
```powershell
winget install --id Git.Git -e
```
Portable Git also works if its `bin` directory is on `PATH`.
## Related docs
- [whatsapp](/channels/whatsapp)

View File

@@ -32,6 +32,7 @@ changing config.
| ---------------------------------------------------- | ------------------------------------------------ | ------------------------------------------------------------------------- |
| ChatGPT/Codex subscription with native Codex runtime | `openai/gpt-5.5` plus `agentRuntime.id: "codex"` | Recommended Codex setup for most users. Sign in with `openai-codex` auth. |
| Direct API-key billing | `openai/gpt-5.5` | Set `OPENAI_API_KEY` or run OpenAI API-key onboarding. |
| Latest ChatGPT Instant API alias | `openai/chat-latest` | Direct API-key only. Moving alias for experiments, not the default. |
| ChatGPT/Codex subscription auth through PI | `openai-codex/gpt-5.5` | Use only when you intentionally want the normal PI runner. |
| Image generation or editing | `openai/gpt-image-2` | Works with either `OPENAI_API_KEY` or OpenAI Codex OAuth. |
| Transparent-background images | `openai/gpt-image-1.5` | Use `outputFormat=png` or `webp` and `openai.background=transparent`. |
@@ -165,6 +166,23 @@ Choose your preferred auth method and follow the setup steps.
}
```
To try ChatGPT's current Instant model from the OpenAI API, set the model
to `openai/chat-latest`:
```json5
{
env: { OPENAI_API_KEY: "sk-..." },
agents: { defaults: { model: { primary: "openai/chat-latest" } } },
}
```
`chat-latest` is a moving alias. OpenAI documents it as the latest Instant
model used in ChatGPT and recommends `gpt-5.5` for production API usage, so
keep `openai/gpt-5.5` as the stable default unless you explicitly want that
alias behavior. The alias currently accepts only `medium` text verbosity, so
OpenClaw normalizes incompatible OpenAI text-verbosity overrides for this
model.
<Warning>
OpenClaw does **not** expose `openai/gpt-5.3-codex-spark`. Live OpenAI API requests reject that model, and the current Codex catalog does not expose it either.
</Warning>
@@ -265,6 +283,14 @@ Choose your preferred auth method and follow the setup steps.
intentional. Switch to `openai/<model>` plus `agentRuntime.id: "codex"` when
you want native Codex app-server execution.
`doctor --fix` does not move a working `openai-codex/*` Codex OAuth route to
direct `openai/*` API-key billing. If an earlier repair already rewrote a
PI Codex OAuth setup to `openai/*` and no direct OpenAI auth is available,
rerun `openclaw doctor --fix` to recover the route back to
`openai-codex/*`. If direct OpenAI auth is also available, doctor warns and
leaves the mixed-auth route unchanged until you confirm whether direct
OpenAI API auth or Codex OAuth through PI is intended.
### Context window cap
OpenClaw treats model metadata and the runtime context cap as separate values.

View File

@@ -59,12 +59,13 @@ the maintainer-only release runbook.
intentionally carried.
4. Create `release/YYYY.M.D` from current `main`; do not do normal release work
directly on `main`.
5. Bump every required version location for the intended tag, run
`pnpm plugins:sync` so publishable plugin packages share the release
version and compatibility metadata, then run the local deterministic preflight:
5. Bump every required version location for the intended tag, then run
`pnpm release:prep`. It refreshes plugin versions, plugin inventory, config
schema, bundled channel config metadata, config docs baseline, plugin SDK
exports, and plugin SDK API baseline in the right order. Commit any generated
drift before tagging. Then run the local deterministic preflight:
`pnpm check:test-types`, `pnpm check:architecture`,
`pnpm build && pnpm ui:build`, `pnpm plugins:sync:check`, and
`pnpm release:check`.
`pnpm build && pnpm ui:build`, and `pnpm release:check`.
6. Run `OpenClaw NPM Release` with `preflight_only=true`. Before a tag exists,
a full 40-character release-branch SHA is allowed for validation-only
preflight. Save the successful `preflight_run_id`.
@@ -77,10 +78,19 @@ the maintainer-only release runbook.
prior evidence stale.
9. For beta, tag `vYYYY.M.D-beta.N`, then run `OpenClaw Release Publish` from
the matching `release/YYYY.M.D` branch. It verifies `pnpm plugins:sync:check`,
publishes all publishable plugin packages to npm first, publishes the same
set to ClawHub second as ClawPack npm-pack tarballs, and then promotes the
prepared OpenClaw npm preflight artifact with the matching dist-tag. After
publish, run post-publish package
dispatches all publishable plugin packages to npm and the same set to
ClawHub in parallel, and then promotes the prepared OpenClaw npm preflight
artifact with the matching dist-tag as soon as plugin npm publish succeeds.
ClawHub publishing may still be running while OpenClaw npm publishes, but the
release publish workflow prints the child run IDs immediately. By default it
does not wait for ClawHub after dispatching it, so OpenClaw npm availability
is not blocked by slower ClawHub approvals or registry work; set
`wait_for_clawhub=true` when ClawHub must block workflow completion. The
ClawHub path retries transient CLI dependency install failures, publishes
preview-passing plugins even when one preview cell flakes, and ends with
registry verification for every expected plugin version so partial publishes
remain visible and retryable. After publish, run
the post-publish package
acceptance against the published `openclaw@YYYY.M.D-beta.N` or
`openclaw@beta` package. If a pushed or published prerelease needs a fix,
cut the next matching prerelease number; do not delete or rewrite the old
@@ -105,12 +115,13 @@ the maintainer-only release runbook.
- Run `pnpm build && pnpm ui:build` before `pnpm release:check` so the expected
`dist/*` release artifacts and Control UI bundle exist for the pack
validation step
- Run `pnpm plugins:sync` after the root version bump and before tagging. It
updates publishable plugin package versions, OpenClaw peer/API compatibility
metadata, build metadata, and plugin changelog stubs to match the core
release version. `pnpm plugins:sync:check` is the non-mutating release guard;
the publish workflow fails before any registry mutation if this step was
forgotten.
- Run `pnpm release:prep` after the root version bump and before tagging. It
runs every deterministic release generator that commonly drifts after a
version/config/API change: plugin versions, plugin inventory, base config
schema, bundled channel config metadata, config docs baseline, plugin SDK
exports, and plugin SDK API baseline. `pnpm release:check` re-runs those
guards in check mode and reports every generated drift failure it finds in one
pass before running package release checks.
- Run the manual `Full Release Validation` workflow before release approval to
kick off all pre-release test boxes from one entrypoint. It accepts a branch,
tag, or full commit SHA, dispatches manual `CI`, and dispatches

View File

@@ -27,6 +27,14 @@ Child workflows use the trusted workflow ref for the harness and the input
`ref` for the candidate under test. That keeps new validation logic available
when validating an older release branch or tag.
Plugin publish validation is intentionally split from core package publication.
`OpenClaw Release Publish` dispatches npm plugin publishing and ClawHub
publishing in parallel, starts the core npm publish after plugin npm succeeds,
and keeps waiting for ClawHub. The ClawHub child retries transient CLI
dependency install failures, publishes preview-passing plugins when a single
preview cell flakes, and then verifies every expected package/version through
the ClawHub API so a partial publish still fails loudly and can be rerun.
By default, `release_profile=stable` runs the release-blocking lanes and skips
the exhaustive live/Docker soak. Pass `run_release_soak=true` to include the
soak lanes on a stable run. `release_profile=full` always enables soak lanes so

View File

@@ -233,6 +233,8 @@ The config shape is identical to `approvals.exec`: `enabled`, `mode`, `agentFilt
Channels that support shared interactive replies render the same approval buttons for both exec and
plugin approvals. Channels without shared interactive UI fall back to plain text with `/approve`
instructions.
Plugin approval requests may restrict the available decisions. Approval surfaces use the request's
declared decision set, and the Gateway rejects attempts to submit a decision that was not offered.
### Same-chat approvals on any channel

View File

@@ -133,6 +133,9 @@ managed npm root. After npm finishes, OpenClaw verifies the installed
`package-lock.json` entry still matches the resolved version and integrity. If
npm writes different package metadata, the install fails and the managed package
is rolled back instead of accepting a different plugin artifact.
Managed npm roots also inherit OpenClaw's package-level npm `overrides`, so
security pins that protect the packaged host also apply to hoisted external
plugin dependencies.
Source checkouts are pnpm workspaces. If you clone OpenClaw to hack on bundled
plugins, run `pnpm install`; OpenClaw then loads bundled plugins from

View File

@@ -152,7 +152,7 @@ Current source-of-truth:
- `/help` shows the short help summary.
- `/commands` shows the generated command catalog.
- `/tools [compact|verbose]` shows what the current agent can use right now.
- `/status` shows execution/runtime status, including `Execution`/`Runtime` labels and provider usage/quota when available.
- `/status` shows execution/runtime status, Gateway and system uptime, plus provider usage/quota when available.
- `/diagnostics [note]` is the owner-only support-report flow for Gateway bugs and Codex harness runs. It asks for explicit exec approval every time before running `openclaw gateway diagnostics export --json`; do not approve diagnostics with an allow-all rule. After approval, it sends a pasteable report with the local bundle path, manifest summary, privacy notes, and relevant session ids. In group chats, the approval prompt and report go to the owner privately. When the active session uses the OpenAI Codex harness, the same approval also sends relevant Codex feedback to OpenAI servers and the completed reply lists the OpenClaw session ids, Codex thread ids, and `codex resume <thread-id>` commands. See [Diagnostics Export](/gateway/diagnostics).
- `/crestodian <request>` runs the Crestodian setup and repair helper from an owner DM.
- `/tasks` lists active/recent background tasks for the current session.

View File

@@ -198,9 +198,9 @@ role or use `first_frame` for single-image image-to-video.
### Style controls
<ParamField path="aspectRatio" type="string">
`1:1`, `2:3`, `3:2`, `3:4`, `4:3`, `4:5`, `5:4`, `9:16`, `16:9`, `21:9`, or `adaptive`.
Aspect-ratio hint such as `1:1`, `16:9`, `9:16`, `adaptive`, or a provider-specific value. OpenClaw normalizes or ignores unsupported values per provider.
</ParamField>
<ParamField path="resolution" type="string">`480P`, `720P`, `768P`, or `1080P`.</ParamField>
<ParamField path="resolution" type="string">Resolution hint such as `480P`, `720P`, `768P`, `1080P`, `4K`, or a provider-specific value. OpenClaw normalizes or ignores unsupported values per provider.</ParamField>
<ParamField path="durationSeconds" type="number">
Target duration in seconds (rounded to nearest provider-supported value).
</ParamField>

View File

@@ -231,12 +231,13 @@ fallbacks after its dedicated web-search config and `GEMINI_API_KEY`. See the
provider pages for examples.
`tools.web.search.provider` is validated against the web-search provider ids
declared by bundled and installed plugin manifests. A typo such as `"brvae"`
fails config validation instead of silently falling back to auto-detection. If a
configured provider only has stale plugin evidence, such as a leftover
`plugins.entries.<plugin>` block after uninstalling a third-party plugin,
OpenClaw keeps startup resilient and reports a warning so you can reinstall the
plugin or run `openclaw doctor --fix` to clean up the stale config.
declared by bundled and installed plugin manifests, plus known installable
provider plugins. A typo such as `"brvae"` fails config validation instead of
silently falling back to auto-detection. If the configured provider is known but
the owning plugin is unavailable, OpenClaw keeps startup resilient and reports a
warning so you can run `openclaw doctor --fix` to install or enable the plugin.
The same warning behavior applies to stale plugin evidence, such as a leftover
`plugins.entries.<plugin>` block after uninstalling a third-party plugin.
`web_fetch` fallback provider selection is separate:

View File

@@ -82,7 +82,7 @@ Notes:
- Model picker: list available models and set the session override.
- Agent picker: choose a different agent.
- Session picker: shows only sessions for the current agent.
- Session picker: shows up to 50 sessions for the current agent updated in the last 7 days. Use `/session <key>` to jump to an older known session.
- Settings: toggle deliver, tool output expansion, and thinking visibility.
## Keyboard shortcuts

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/acpx",
"version": "2026.5.4",
"version": "2026.5.7",
"description": "OpenClaw ACP runtime backend",
"repository": {
"type": "git",
@@ -25,10 +25,10 @@
"minHostVersion": ">=2026.4.25"
},
"compat": {
"pluginApi": ">=2026.5.4"
"pluginApi": ">=2026.5.7"
},
"build": {
"openclawVersion": "2026.5.4",
"openclawVersion": "2026.5.7",
"staticAssets": [
{
"source": "./src/runtime-internals/mcp-proxy.mjs",

View File

@@ -420,6 +420,108 @@ describe("active-memory plugin", () => {
expect(runEmbeddedPiAgent).toHaveBeenCalledTimes(1);
});
it("blocks gateway callers without admin scope from changing global active-memory config", async () => {
const command = registeredCommands["active-memory"];
for (const { args, gatewayClientScopes } of [
{ args: "off --global", gatewayClientScopes: ["operator.write"] },
{ args: "on --global", gatewayClientScopes: ["operator.write"] },
{ args: "disable --global", gatewayClientScopes: ["operator.write"] },
{ args: "enable --global", gatewayClientScopes: ["operator.write"] },
{ args: "disabled --global", gatewayClientScopes: ["operator.write"] },
{ args: "enabled --global", gatewayClientScopes: ["operator.write"] },
{ args: "off --global", gatewayClientScopes: [] },
]) {
const result = await command.handler({
channel: "gateway",
isAuthorizedSender: true,
gatewayClientScopes,
args,
commandBody: `/active-memory ${args}`,
config: {},
requestConversationBinding: async () => ({ status: "error", message: "unsupported" }),
detachConversationBinding: async () => ({ removed: false }),
getCurrentConversationBinding: async () => null,
});
expect(result.text).toContain("global enable/disable changes require operator.admin");
}
expect(api.runtime.config.replaceConfigFile).not.toHaveBeenCalled();
});
it("allows admin-scoped gateway callers to change global active-memory config", async () => {
const command = registeredCommands["active-memory"];
const result = await command.handler({
channel: "gateway",
isAuthorizedSender: true,
gatewayClientScopes: ["operator.admin"],
args: "off --global",
commandBody: "/active-memory off --global",
config: {},
requestConversationBinding: async () => ({ status: "error", message: "unsupported" }),
detachConversationBinding: async () => ({ removed: false }),
getCurrentConversationBinding: async () => null,
});
expect(result.text).toBe("Active Memory: off globally.");
expect(api.runtime.config.replaceConfigFile).toHaveBeenCalledTimes(1);
expect(configFile).toMatchObject({
plugins: {
entries: {
"active-memory": {
enabled: true,
config: {
enabled: false,
agents: ["main"],
},
},
},
},
});
});
it("keeps write-scoped gateway callers on non-global-write active-memory paths", async () => {
const command = registeredCommands["active-memory"];
const sessionKey = "agent:main:write-scoped-active-memory";
hoisted.sessionStore[sessionKey] = {
sessionId: "s-write-scoped-active-memory",
updatedAt: 0,
};
const globalStatusResult = await command.handler({
channel: "gateway",
isAuthorizedSender: true,
gatewayClientScopes: ["operator.write"],
args: "status --global",
commandBody: "/active-memory status --global",
config: {},
requestConversationBinding: async () => ({ status: "error", message: "unsupported" }),
detachConversationBinding: async () => ({ removed: false }),
getCurrentConversationBinding: async () => null,
});
expect(globalStatusResult.text).toBe("Active Memory: on globally.");
expect(api.runtime.config.replaceConfigFile).not.toHaveBeenCalled();
const sessionOffResult = await command.handler({
channel: "gateway",
isAuthorizedSender: true,
gatewayClientScopes: ["operator.write"],
sessionKey,
args: "off",
commandBody: "/active-memory off",
config: {},
requestConversationBinding: async () => ({ status: "error", message: "unsupported" }),
detachConversationBinding: async () => ({ removed: false }),
getCurrentConversationBinding: async () => null,
});
expect(sessionOffResult.text).toBe("Active Memory: off for this session.");
expect(api.runtime.config.replaceConfigFile).not.toHaveBeenCalled();
});
it("uses live runtime config for before_prompt_build enablement", async () => {
configFile = {
plugins: {

View File

@@ -783,6 +783,13 @@ function updateActiveMemoryGlobalEnabledInConfig(
};
}
function requiresAdminToMutateActiveMemoryGlobal(gatewayClientScopes?: readonly string[]): boolean {
return Array.isArray(gatewayClientScopes) && !gatewayClientScopes.includes("operator.admin");
}
const ACTIVE_MEMORY_GLOBAL_MUTATION_ADMIN_REQUIRED_TEXT =
"⚠️ /active-memory global enable/disable changes require operator.admin for gateway clients.";
function normalizePluginConfig(pluginConfig: unknown): ResolvedActiveRecallPluginConfig {
const raw = (
pluginConfig && typeof pluginConfig === "object" ? pluginConfig : {}
@@ -2818,6 +2825,11 @@ export default definePluginEntry({
text: `Active Memory: ${isActiveMemoryGloballyEnabled(currentConfig) ? "on" : "off"} globally.`,
};
}
if (requiresAdminToMutateActiveMemoryGlobal(ctx.gatewayClientScopes)) {
return {
text: ACTIVE_MEMORY_GLOBAL_MUTATION_ADMIN_REQUIRED_TEXT,
};
}
if (action === "on" || action === "enable" || action === "enabled") {
const nextConfig = updateActiveMemoryGlobalEnabledInConfig(currentConfig, true);
await api.runtime.config.replaceConfigFile({

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/alibaba-provider",
"version": "2026.5.4",
"version": "2026.5.7",
"private": true,
"description": "OpenClaw Alibaba Model Studio video provider plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/amazon-bedrock-mantle-provider",
"version": "2026.5.4",
"version": "2026.5.7",
"private": true,
"description": "OpenClaw Amazon Bedrock Mantle (OpenAI-compatible) provider plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/amazon-bedrock-provider",
"version": "2026.5.4",
"version": "2026.5.7",
"private": true,
"description": "OpenClaw Amazon Bedrock provider plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/anthropic-vertex-provider",
"version": "2026.5.4",
"version": "2026.5.7",
"private": true,
"description": "OpenClaw Anthropic Vertex provider plugin",
"type": "module",

View File

@@ -257,6 +257,30 @@ describe("anthropic provider replay hooks", () => {
).toBe(false);
});
it("does not forward-compat case-mismatched Anthropic model ids", async () => {
const provider = await registerSingleProviderPlugin(anthropicPlugin);
const resolved = provider.resolveDynamicModel?.({
provider: "anthropic",
modelId: "CLAUDE-OPUS-4-7",
modelRegistry: createModelRegistry([
{
id: "claude-opus-4-6",
name: "Claude Opus 4.6",
provider: "anthropic",
api: "anthropic-messages",
reasoning: true,
input: ["text", "image"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 200_000,
maxTokens: 32_000,
} as ProviderRuntimeModel,
]),
} as ProviderResolveDynamicModelContext);
expect(resolved).toBeUndefined();
});
it("normalizes exact claude opus 4.7 variants to 1M context", async () => {
const provider = await registerSingleProviderPlugin(anthropicPlugin);

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/anthropic-provider",
"version": "2026.5.4",
"version": "2026.5.7",
"private": true,
"description": "OpenClaw Anthropic provider plugin",
"type": "module",

View File

@@ -214,6 +214,9 @@ function resolveAnthropic46ForwardCompatModel(params: {
}): ProviderRuntimeModel | undefined {
const trimmedModelId = params.ctx.modelId.trim();
const lower = normalizeLowercaseStringOrEmpty(trimmedModelId);
if (trimmedModelId !== lower) {
return undefined;
}
const is46Model =
lower === params.dashModelId ||
lower === params.dotModelId ||

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/arcee-provider",
"version": "2026.5.4",
"version": "2026.5.7",
"private": true,
"description": "OpenClaw Arcee provider plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/azure-speech",
"version": "2026.5.4",
"version": "2026.5.7",
"private": true,
"description": "OpenClaw Azure Speech plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/bluebubbles",
"version": "2026.5.4",
"version": "2026.5.7",
"description": "OpenClaw BlueBubbles channel plugin",
"repository": {
"type": "git",
@@ -12,7 +12,7 @@
"openclaw": "workspace:*"
},
"peerDependencies": {
"openclaw": ">=2026.5.4"
"openclaw": ">=2026.5.7"
},
"peerDependenciesMeta": {
"openclaw": {
@@ -53,10 +53,10 @@
"minHostVersion": ">=2026.4.10"
},
"compat": {
"pluginApi": ">=2026.5.4"
"pluginApi": ">=2026.5.7"
},
"build": {
"openclawVersion": "2026.5.4"
"openclawVersion": "2026.5.7"
},
"release": {
"publishToClawHub": true,

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/bonjour",
"version": "2026.5.4",
"version": "2026.5.7",
"description": "OpenClaw Bonjour/mDNS gateway discovery",
"type": "module",
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/brave-plugin",
"version": "2026.5.4",
"version": "2026.5.7",
"description": "OpenClaw Brave plugin",
"repository": {
"type": "git",
@@ -20,10 +20,10 @@
"minHostVersion": ">=2026.4.10"
},
"compat": {
"pluginApi": ">=2026.5.4"
"pluginApi": ">=2026.5.7"
},
"build": {
"openclawVersion": "2026.5.4"
"openclawVersion": "2026.5.7"
},
"release": {
"publishToClawHub": true,

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/browser-plugin",
"version": "2026.5.4",
"version": "2026.5.7",
"private": true,
"description": "OpenClaw browser tool plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/byteplus-provider",
"version": "2026.5.4",
"version": "2026.5.7",
"private": true,
"description": "OpenClaw BytePlus provider plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/cerebras-provider",
"version": "2026.5.4",
"version": "2026.5.7",
"private": true,
"description": "OpenClaw Cerebras provider plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/chutes-provider",
"version": "2026.5.4",
"version": "2026.5.7",
"private": true,
"description": "OpenClaw Chutes.ai provider plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/cloudflare-ai-gateway-provider",
"version": "2026.5.4",
"version": "2026.5.7",
"private": true,
"description": "OpenClaw Cloudflare AI Gateway provider plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/codex",
"version": "2026.5.4",
"version": "2026.5.7",
"description": "OpenClaw Codex harness and model provider plugin",
"repository": {
"type": "git",
@@ -27,10 +27,10 @@
"minHostVersion": ">=2026.5.1-beta.1"
},
"compat": {
"pluginApi": ">=2026.5.4"
"pluginApi": ">=2026.5.7"
},
"build": {
"openclawVersion": "2026.5.4"
"openclawVersion": "2026.5.7"
},
"release": {
"publishToClawHub": true,

View File

@@ -660,6 +660,164 @@ describe("CodexAppServerEventProjector", () => {
expect(result.itemLifecycle).toMatchObject({ compactionCount: 1 });
});
it("synthesizes normalized tool progress for Codex-native tool items", async () => {
const onAgentEvent = vi.fn();
const projector = await createProjector({ ...(await createParams()), onAgentEvent });
await projector.handleNotification(
forCurrentTurn("item/started", {
item: {
type: "commandExecution",
id: "cmd-1",
command: "pnpm test extensions/codex",
cwd: "/workspace",
processId: null,
source: "agent",
status: "inProgress",
commandActions: [],
aggregatedOutput: null,
exitCode: null,
durationMs: null,
},
}),
);
await projector.handleNotification(
forCurrentTurn("item/completed", {
item: {
type: "commandExecution",
id: "cmd-1",
command: "pnpm test extensions/codex",
cwd: "/workspace",
processId: null,
source: "agent",
status: "completed",
commandActions: [],
aggregatedOutput: "ok",
exitCode: 0,
durationMs: 42,
},
}),
);
expect(onAgentEvent).toHaveBeenCalledWith({
stream: "item",
data: expect.objectContaining({
phase: "start",
kind: "command",
name: "bash",
itemId: "cmd-1",
suppressChannelProgress: true,
}),
});
expect(onAgentEvent).toHaveBeenCalledWith({
stream: "tool",
data: expect.objectContaining({
phase: "start",
name: "bash",
itemId: "cmd-1",
toolCallId: "cmd-1",
args: { command: "pnpm test extensions/codex", cwd: "/workspace" },
}),
});
expect(onAgentEvent).toHaveBeenCalledWith({
stream: "tool",
data: expect.objectContaining({
phase: "result",
name: "bash",
itemId: "cmd-1",
toolCallId: "cmd-1",
status: "completed",
isError: false,
result: expect.objectContaining({ exitCode: 0, durationMs: 42 }),
}),
});
});
it("marks declined Codex-native tool results as non-success", async () => {
const onAgentEvent = vi.fn();
const projector = await createProjector({ ...(await createParams()), onAgentEvent });
await projector.handleNotification(
forCurrentTurn("item/completed", {
item: {
type: "commandExecution",
id: "cmd-declined",
command: "pnpm test extensions/codex",
cwd: "/workspace",
processId: null,
source: "agent",
status: "declined",
commandActions: [],
aggregatedOutput: null,
exitCode: null,
durationMs: null,
},
}),
);
expect(onAgentEvent).toHaveBeenCalledWith({
stream: "item",
data: expect.objectContaining({
phase: "end",
kind: "command",
name: "bash",
itemId: "cmd-declined",
status: "blocked",
suppressChannelProgress: true,
}),
});
expect(onAgentEvent).toHaveBeenCalledWith({
stream: "tool",
data: expect.objectContaining({
phase: "result",
name: "bash",
itemId: "cmd-declined",
toolCallId: "cmd-declined",
status: "blocked",
isError: true,
}),
});
});
it("leaves Codex dynamic tool item progress to item/tool/call normalization", async () => {
const onAgentEvent = vi.fn();
const projector = await createProjector({ ...(await createParams()), onAgentEvent });
await projector.handleNotification(
forCurrentTurn("item/started", {
item: {
type: "dynamicToolCall",
id: "call-1",
namespace: null,
tool: "message",
arguments: { action: "send" },
status: "inProgress",
contentItems: null,
success: null,
durationMs: null,
},
}),
);
expect(onAgentEvent).toHaveBeenCalledWith(
expect.objectContaining({
stream: "item",
data: expect.objectContaining({
phase: "start",
kind: "tool",
name: "message",
suppressChannelProgress: true,
}),
}),
);
expect(onAgentEvent).not.toHaveBeenCalledWith(
expect.objectContaining({
stream: "tool",
data: expect.objectContaining({ phase: "start", name: "message" }),
}),
);
});
it("emits verbose tool summaries through onToolResult", async () => {
const onToolResult = vi.fn();
const projector = await createProjector({

View File

@@ -30,6 +30,11 @@ import {
import { readRecentCodexRateLimits, rememberCodexRateLimits } from "./rate-limit-cache.js";
import { formatCodexUsageLimitErrorMessage } from "./rate-limits.js";
import { readCodexMirroredSessionHistoryMessages } from "./session-history.js";
import {
resolveCodexToolProgressDetailMode,
sanitizeCodexAgentEventRecord,
sanitizeCodexToolArguments,
} from "./tool-progress-normalization.js";
import { attachCodexMirrorIdentity } from "./transcript-mirror.js";
export type CodexAppServerToolTelemetry = {
@@ -396,6 +401,7 @@ export class CodexAppServerEventProjector {
});
}
this.emitStandardItemEvent({ phase: "start", item });
this.emitNormalizedToolItemEvent({ phase: "start", item });
this.emitToolResultSummary(item);
this.emitAgentEvent({
stream: "codex_app_server.item",
@@ -449,6 +455,7 @@ export class CodexAppServerEventProjector {
}
this.recordToolMeta(item);
this.emitStandardItemEvent({ phase: "end", item });
this.emitNormalizedToolItemEvent({ phase: "result", item });
this.emitToolResultSummary(item);
this.emitToolResultOutput(item);
this.emitAgentEvent({
@@ -656,6 +663,7 @@ export class CodexAppServerEventProjector {
return;
}
const meta = itemMeta(item, this.toolProgressDetailMode());
const suppressChannelProgress = shouldSuppressChannelProgressForItem(item);
this.emitAgentEvent({
stream: "item",
data: {
@@ -666,6 +674,42 @@ export class CodexAppServerEventProjector {
status: params.phase === "start" ? "running" : itemStatus(item),
...(itemName(item) ? { name: itemName(item) } : {}),
...(meta ? { meta } : {}),
...(suppressChannelProgress ? { suppressChannelProgress: true } : {}),
},
});
}
private emitNormalizedToolItemEvent(params: {
phase: "start" | "result";
item: CodexThreadItem | undefined;
}): void {
const { item } = params;
if (!item || !shouldSynthesizeToolProgressForItem(item)) {
return;
}
const name = itemName(item);
if (!name) {
return;
}
const meta = itemMeta(item, this.toolProgressDetailMode());
const args = params.phase === "start" ? itemToolArgs(item) : undefined;
const status = params.phase === "result" ? itemStatus(item) : "running";
this.emitAgentEvent({
stream: "tool",
data: {
phase: params.phase,
name,
itemId: item.id,
toolCallId: item.id,
...(meta ? { meta } : {}),
...(args ? { args } : {}),
...(params.phase === "result"
? {
status,
isError: isNonSuccessItemStatus(status),
...itemToolResult(item),
}
: {}),
},
});
}
@@ -743,7 +787,7 @@ export class CodexAppServerEventProjector {
}
private toolProgressDetailMode(): ToolProgressDetailMode {
return this.params.toolProgressDetail === "raw" ? "raw" : "explain";
return resolveCodexToolProgressDetailMode(this.params.toolProgressDetail);
}
private recordToolMeta(item: CodexThreadItem | undefined): void {
@@ -1074,17 +1118,24 @@ function itemTitle(item: CodexThreadItem): string {
}
}
function itemStatus(item: CodexThreadItem): "completed" | "failed" | "running" {
function itemStatus(item: CodexThreadItem): "completed" | "failed" | "running" | "blocked" {
const status = readItemString(item, "status");
if (status === "failed") {
return "failed";
}
if (status === "declined") {
return "blocked";
}
if (status === "inProgress" || status === "running") {
return "running";
}
return "completed";
}
function isNonSuccessItemStatus(status: ReturnType<typeof itemStatus>): boolean {
return status === "failed" || status === "blocked";
}
function itemName(item: CodexThreadItem): string | undefined {
if (item.type === "dynamicToolCall" && typeof item.tool === "string") {
return item.tool;
@@ -1105,6 +1156,78 @@ function itemName(item: CodexThreadItem): string | undefined {
return undefined;
}
function shouldSynthesizeToolProgressForItem(item: CodexThreadItem): boolean {
switch (item.type) {
case "commandExecution":
case "fileChange":
case "webSearch":
case "mcpToolCall":
return true;
default:
return false;
}
}
function shouldSuppressChannelProgressForItem(item: CodexThreadItem): boolean {
if (shouldSynthesizeToolProgressForItem(item)) {
return true;
}
// Dynamic OpenClaw tool requests are emitted at the item/tool/call request
// boundary in run-attempt.ts. Re-emitting item notifications to channels can
// duplicate start/result progress when the app-server sends both signals.
return item.type === "dynamicToolCall";
}
function itemToolArgs(item: CodexThreadItem): Record<string, unknown> | undefined {
if (item.type === "commandExecution") {
return sanitizeCodexAgentEventRecord({
command: item.command,
...(typeof item.cwd === "string" ? { cwd: item.cwd } : {}),
});
}
if (item.type === "webSearch" && typeof item.query === "string") {
return sanitizeCodexAgentEventRecord({ query: item.query });
}
if (item.type === "mcpToolCall") {
return sanitizeCodexToolArguments(item.arguments);
}
return undefined;
}
function itemToolResult(item: CodexThreadItem): { result?: Record<string, unknown> } {
if (item.type === "commandExecution") {
return {
result: sanitizeCodexAgentEventRecord({
status: item.status,
exitCode: item.exitCode,
durationMs: item.durationMs,
}),
};
}
if (item.type === "fileChange") {
return {
result: sanitizeCodexAgentEventRecord({
status: item.status,
changes: item.changes.map((change) => ({ path: change.path, kind: change.kind })),
}),
};
}
if (item.type === "mcpToolCall") {
return {
result: sanitizeCodexAgentEventRecord({
status: item.status,
durationMs: item.durationMs,
...(item.error ? { error: item.error } : {}),
...(item.result ? { result: item.result } : {}),
}),
};
}
if (item.type === "webSearch") {
return { result: sanitizeCodexAgentEventRecord({ status: "completed" }) };
}
return {};
}
function itemMeta(
item: CodexThreadItem,
detailMode: ToolProgressDetailMode = "explain",

View File

@@ -180,6 +180,7 @@ function createAppServerHarness(
) {
const requests: Array<{ method: string; params: unknown }> = [];
let notify: (notification: CodexServerNotification) => Promise<void> = async () => undefined;
let handleServerRequest: AppServerRequestHandler | undefined;
const request = vi.fn(async (method: string, params?: unknown) => {
requests.push({ method, params });
return requestImpl(method, params);
@@ -194,11 +195,22 @@ function createAppServerHarness(
notify = handler;
return () => undefined;
},
addRequestHandler: () => () => undefined,
addRequestHandler: (handler: AppServerRequestHandler) => {
handleServerRequest = handler;
return () => undefined;
},
} as never;
},
);
const waitForServerRequestHandler = async () => {
await vi.waitFor(() => expect(handleServerRequest).toBeTypeOf("function"), {
interval: 1,
timeout: 30_000,
});
return handleServerRequest!;
};
return {
request,
requests,
@@ -220,6 +232,11 @@ function createAppServerHarness(
async notify(notification: CodexServerNotification) {
await notify(notification);
},
waitForServerRequestHandler,
async handleServerRequest(request: Parameters<AppServerRequestHandler>[0]) {
const handler = await waitForServerRequestHandler();
return handler(request);
},
async completeTurn(params: { threadId: string; turnId: string }) {
await notify({
method: "turn/completed",
@@ -346,14 +363,35 @@ function createNamedDynamicTool(
};
}
type AppServerRequestHandler = (request: {
id: string | number;
method: string;
params?: unknown;
}) => Promise<unknown>;
function extractRelayIdFromThreadRequest(params: unknown): string {
const command = (
params as {
config?: {
"hooks.PreToolUse"?: Array<{ hooks?: Array<{ command?: string }> }>;
};
const config = (params as { config?: Record<string, unknown> }).config;
let command: string | undefined;
for (const key of [
"hooks.PreToolUse",
"hooks.PostToolUse",
"hooks.PermissionRequest",
"hooks.Stop",
]) {
const entries = config?.[key];
if (!Array.isArray(entries)) {
continue;
}
).config?.["hooks.PreToolUse"]?.[0]?.hooks?.[0]?.command;
for (const entry of entries as Array<{ hooks?: Array<{ command?: string }> }>) {
command = entry.hooks?.find((hook) => typeof hook.command === "string")?.command;
if (command) {
break;
}
}
if (command) {
break;
}
}
const match = command?.match(/--relay-id ([^ ]+)/);
if (!match?.[1]) {
throw new Error(`relay id missing from command: ${command}`);
@@ -622,6 +660,93 @@ describe("runCodexAppServerAttempt", () => {
});
});
it("emits normalized tool progress around app-server dynamic tool requests", async () => {
const harness = createStartedThreadHarness();
const onRunAgentEvent = vi.fn();
const globalAgentEvents: AgentEventPayload[] = [];
onAgentEvent((event) => globalAgentEvents.push(event));
const params = createParams(
path.join(tempDir, "session.jsonl"),
path.join(tempDir, "workspace"),
);
params.onAgentEvent = onRunAgentEvent;
const run = runCodexAppServerAttempt(params);
await harness.waitForMethod("turn/start");
await expect(
harness.handleServerRequest({
id: "request-tool-1",
method: "item/tool/call",
params: {
threadId: "thread-1",
turnId: "turn-1",
callId: "call-1",
namespace: null,
tool: "message",
arguments: {
action: "send",
token: "plain-secret-value-12345",
text: "hello",
},
},
}),
).resolves.toMatchObject({
success: false,
contentItems: [
{
type: "inputText",
text: expect.stringMatching(
/^(Unknown OpenClaw tool: message|Action send requires a target\.)$/u,
),
},
],
});
await harness.completeTurn({ threadId: "thread-1", turnId: "turn-1" });
await run;
const agentEvents = onRunAgentEvent.mock.calls.map(([event]) => event);
expect(agentEvents).toEqual(
expect.arrayContaining([
expect.objectContaining({
stream: "tool",
data: expect.objectContaining({
phase: "start",
name: "message",
toolCallId: "call-1",
args: expect.objectContaining({
action: "send",
token: "plain-…2345",
text: "hello",
}),
}),
}),
expect.objectContaining({
stream: "tool",
data: expect.objectContaining({
phase: "result",
name: "message",
toolCallId: "call-1",
isError: true,
result: expect.objectContaining({ success: false }),
}),
}),
]),
);
expect(JSON.stringify(agentEvents)).not.toContain("plain-secret-value-12345");
expect(globalAgentEvents).toEqual(
expect.arrayContaining([
expect.objectContaining({
runId: "run-1",
sessionKey: "agent:main:session-1",
stream: "tool",
data: expect.objectContaining({ phase: "start", name: "message" }),
}),
]),
);
});
it("releases the session when Codex never completes after a dynamic tool response", async () => {
let handleRequest:
| ((request: { id: string; method: string; params?: unknown }) => Promise<unknown>)
@@ -1078,6 +1203,85 @@ describe("runCodexAppServerAttempt", () => {
expect(nativeHookRelayTesting.getNativeHookRelayRegistrationForTests(relayId)).toBeUndefined();
});
it("lets Codex app-server approval modes own native permission requests by default", async () => {
const sessionFile = path.join(tempDir, "session.jsonl");
const workspaceDir = path.join(tempDir, "workspace");
const harness = createStartedThreadHarness();
const run = runCodexAppServerAttempt(createParams(sessionFile, workspaceDir), {
pluginConfig: {
appServer: {
mode: "guardian",
},
},
});
await harness.waitForMethod("turn/start");
const startRequest = harness.requests.find((request) => request.method === "thread/start");
expect(startRequest?.params).toEqual(
expect.objectContaining({
config: expect.objectContaining({
"features.codex_hooks": true,
"hooks.PreToolUse": expect.any(Array),
"hooks.PostToolUse": expect.any(Array),
"hooks.Stop": expect.any(Array),
}),
}),
);
expect(startRequest?.params).toEqual(
expect.objectContaining({
config: expect.not.objectContaining({
"hooks.PermissionRequest": expect.anything(),
}),
}),
);
const relayId = extractRelayIdFromThreadRequest(startRequest?.params);
expect(nativeHookRelayTesting.getNativeHookRelayRegistrationForTests(relayId)).toMatchObject({
allowedEvents: ["pre_tool_use", "post_tool_use", "before_agent_finalize"],
});
await harness.completeTurn({ threadId: "thread-1", turnId: "turn-1" });
await run;
expect(nativeHookRelayTesting.getNativeHookRelayRegistrationForTests(relayId)).toBeUndefined();
});
it("preserves explicit native permission request relay events in app-server approval modes", async () => {
const sessionFile = path.join(tempDir, "session.jsonl");
const workspaceDir = path.join(tempDir, "workspace");
const harness = createStartedThreadHarness();
const run = runCodexAppServerAttempt(createParams(sessionFile, workspaceDir), {
pluginConfig: {
appServer: {
mode: "guardian",
},
},
nativeHookRelay: {
enabled: true,
events: ["permission_request"],
},
});
await harness.waitForMethod("turn/start");
const startRequest = harness.requests.find((request) => request.method === "thread/start");
expect(startRequest?.params).toEqual(
expect.objectContaining({
config: expect.objectContaining({
"features.codex_hooks": true,
"hooks.PermissionRequest": expect.any(Array),
}),
}),
);
const relayId = extractRelayIdFromThreadRequest(startRequest?.params);
expect(nativeHookRelayTesting.getNativeHookRelayRegistrationForTests(relayId)).toMatchObject({
allowedEvents: ["permission_request"],
});
await harness.completeTurn({ threadId: "thread-1", turnId: "turn-1" });
await run;
expect(nativeHookRelayTesting.getNativeHookRelayRegistrationForTests(relayId)).toBeUndefined();
});
it("reuses the Codex native hook relay id across runs for the same session", async () => {
const sessionFile = path.join(tempDir, "session.jsonl");
const workspaceDir = path.join(tempDir, "workspace");

View File

@@ -58,6 +58,7 @@ import { ensureCodexComputerUse } from "./computer-use.js";
import {
readCodexPluginConfig,
resolveCodexAppServerRuntimeOptions,
type CodexAppServerRuntimeOptions,
type CodexPluginConfig,
} from "./config.js";
import { projectContextEngineAssemblyForCodex } from "./context-engine-projection.js";
@@ -96,6 +97,12 @@ import {
codexDynamicToolsFingerprint,
startOrResumeThread,
} from "./thread-lifecycle.js";
import {
inferCodexDynamicToolMeta,
resolveCodexToolProgressDetailMode,
sanitizeCodexToolArguments,
sanitizeCodexToolResponse,
} from "./tool-progress-normalization.js";
import {
createCodexTrajectoryRecorder,
normalizeCodexTrajectoryError,
@@ -113,6 +120,8 @@ const CODEX_TURN_TERMINAL_IDLE_TIMEOUT_MS = 30 * 60_000;
const CODEX_STEER_ALL_DEBOUNCE_MS = 500;
const LOG_FIELD_MAX_LENGTH = 160;
const CODEX_NATIVE_PROJECT_DOC_BASENAMES = new Set(["agents.md"]);
const CODEX_NATIVE_HOOK_RELAY_EVENTS_WITH_APP_SERVER_APPROVALS =
CODEX_NATIVE_HOOK_RELAY_EVENTS.filter((event) => event !== "permission_request");
const CODEX_BOOTSTRAP_CONTEXT_ORDER = new Map<string, number>([
["soul.md", 10],
["identity.md", 20],
@@ -354,6 +363,10 @@ export async function runCodexAppServerAttempt(
const attemptClientFactory = clientFactory;
const pluginConfig = readCodexPluginConfig(options.pluginConfig);
const appServer = resolveCodexAppServerRuntimeOptions({ pluginConfig });
const nativeHookRelayEvents = resolveCodexNativeHookRelayEvents({
configuredEvents: options.nativeHookRelay?.events,
appServer,
});
const resolvedWorkspace = resolveUserPath(params.workspaceDir);
await fs.mkdir(resolvedWorkspace, { recursive: true });
const sandboxSessionKey =
@@ -551,6 +564,7 @@ export async function runCodexAppServerAttempt(
});
nativeHookRelay = createCodexNativeHookRelay({
options: options.nativeHookRelay,
events: nativeHookRelayEvents,
agentId: sessionAgentId,
sessionId: params.sessionId,
sessionKey: sandboxSessionKey,
@@ -561,7 +575,7 @@ export async function runCodexAppServerAttempt(
const nativeHookRelayConfig = nativeHookRelay
? buildCodexNativeHookRelayConfig({
relay: nativeHookRelay,
events: options.nativeHookRelay?.events,
events: nativeHookRelayEvents,
hookTimeoutSec: options.nativeHookRelay?.hookTimeoutSec,
})
: options.nativeHookRelay?.enabled === false
@@ -963,6 +977,19 @@ export async function runCodexAppServerAttempt(
name: call.tool,
arguments: call.arguments,
});
const toolProgressDetailMode = resolveCodexToolProgressDetailMode(params.toolProgressDetail);
const toolMeta = inferCodexDynamicToolMeta(call, toolProgressDetailMode);
const toolArgs = sanitizeCodexToolArguments(call.arguments);
emitCodexAppServerEvent(params, {
stream: "tool",
data: {
phase: "start",
name: call.tool,
toolCallId: call.callId,
...(toolMeta ? { meta: toolMeta } : {}),
...(toolArgs ? { args: toolArgs } : {}),
},
});
const response = await handleDynamicToolCallWithTimeout({
call,
toolBridge,
@@ -986,6 +1013,17 @@ export async function runCodexAppServerAttempt(
success: response.success,
contentItems: response.contentItems,
});
emitCodexAppServerEvent(params, {
stream: "tool",
data: {
phase: "result",
name: call.tool,
toolCallId: call.callId,
...(toolMeta ? { meta: toolMeta } : {}),
isError: !response.success,
result: sanitizeCodexToolResponse(response),
},
});
return response as JsonValue;
} finally {
activeAppServerTurnRequests = Math.max(0, activeAppServerTurnRequests - 1);
@@ -1395,11 +1433,11 @@ function createCodexNativeHookRelay(params: {
options:
| {
enabled?: boolean;
events?: readonly NativeHookRelayEvent[];
ttlMs?: number;
gatewayTimeoutMs?: number;
}
| undefined;
events: readonly NativeHookRelayEvent[];
agentId: string | undefined;
sessionId: string;
sessionKey: string | undefined;
@@ -1422,7 +1460,7 @@ function createCodexNativeHookRelay(params: {
...(params.sessionKey ? { sessionKey: params.sessionKey } : {}),
...(params.config ? { config: params.config } : {}),
runId: params.runId,
allowedEvents: params.options?.events ?? CODEX_NATIVE_HOOK_RELAY_EVENTS,
allowedEvents: params.events,
ttlMs: params.options?.ttlMs,
signal: params.signal,
command: {
@@ -1431,6 +1469,22 @@ function createCodexNativeHookRelay(params: {
});
}
function resolveCodexNativeHookRelayEvents(params: {
configuredEvents?: readonly NativeHookRelayEvent[];
appServer: Pick<CodexAppServerRuntimeOptions, "approvalPolicy">;
}): readonly NativeHookRelayEvent[] {
if (params.configuredEvents?.length) {
return params.configuredEvents;
}
// Codex emits PermissionRequest before the app-server approval reviewer has
// resolved the command. In native approval modes, let Codex's app-server
// approval bridge own the real escalation instead of surfacing a stale
// pre-guardian OpenClaw plugin approval prompt.
return params.appServer.approvalPolicy === "never"
? CODEX_NATIVE_HOOK_RELAY_EVENTS
: CODEX_NATIVE_HOOK_RELAY_EVENTS_WITH_APP_SERVER_APPROVALS;
}
function buildCodexNativeHookRelayId(params: {
agentId: string | undefined;
sessionId: string;

View File

@@ -0,0 +1,77 @@
import {
inferToolMetaFromArgs,
type EmbeddedRunAttemptParams,
type ToolProgressDetailMode,
} from "openclaw/plugin-sdk/agent-harness-runtime";
import { redactSensitiveFieldValue, redactToolPayloadText } from "openclaw/plugin-sdk/text-runtime";
import {
isJsonObject,
type CodexDynamicToolCallParams,
type CodexDynamicToolCallResponse,
type JsonValue,
} from "./protocol.js";
export function resolveCodexToolProgressDetailMode(
value: EmbeddedRunAttemptParams["toolProgressDetail"],
): ToolProgressDetailMode {
return value === "raw" ? "raw" : "explain";
}
export function sanitizeCodexAgentEventValue(
value: unknown,
seen = new WeakSet<object>(),
): unknown {
if (typeof value === "string") {
return redactToolPayloadText(value);
}
if (Array.isArray(value)) {
if (seen.has(value)) {
return "[Circular]";
}
seen.add(value);
return value.map((entry) => sanitizeCodexAgentEventValue(entry, seen));
}
if (value && typeof value === "object") {
if (seen.has(value)) {
return "[Circular]";
}
seen.add(value);
const out: Record<string, unknown> = {};
for (const [key, child] of Object.entries(value as Record<string, unknown>)) {
out[key] =
typeof child === "string"
? redactSensitiveFieldValue(key, child)
: sanitizeCodexAgentEventValue(child, seen);
}
return out;
}
return value;
}
export function sanitizeCodexAgentEventRecord(
value: Record<string, unknown>,
): Record<string, unknown> {
return sanitizeCodexAgentEventValue(value) as Record<string, unknown>;
}
export function sanitizeCodexToolArguments(
value: JsonValue | undefined,
): Record<string, unknown> | undefined {
if (!isJsonObject(value)) {
return undefined;
}
return sanitizeCodexAgentEventRecord(value);
}
export function sanitizeCodexToolResponse(
response: CodexDynamicToolCallResponse,
): Record<string, unknown> {
return sanitizeCodexAgentEventRecord(response as unknown as Record<string, unknown>);
}
export function inferCodexDynamicToolMeta(
call: Pick<CodexDynamicToolCallParams, "tool" | "arguments">,
detailMode: ToolProgressDetailMode,
): string | undefined {
return inferToolMetaFromArgs(call.tool, call.arguments, { detailMode });
}

View File

@@ -48,7 +48,10 @@ describe("codex conversation binding", () => {
});
beforeEach(() => {
agentRuntimeMocks.ensureAuthProfileStore.mockReturnValue({ version: 1, profiles: {} });
agentRuntimeMocks.ensureAuthProfileStore.mockReturnValue({
version: 1,
profiles: {},
});
agentRuntimeMocks.resolveAuthProfileOrder.mockReturnValue([]);
agentRuntimeMocks.resolveOpenClawAgentDir.mockReturnValue("/agent");
agentRuntimeMocks.resolveProviderIdForAuth.mockImplementation((provider: string) => provider);
@@ -56,7 +59,9 @@ describe("codex conversation binding", () => {
it("uses the default Codex auth profile and omits the public OpenAI provider for new binds", async () => {
const sessionFile = path.join(tempDir, "session.jsonl");
const config = { auth: { order: { "openai-codex": ["openai-codex:default"] } } };
const config = {
auth: { order: { "openai-codex": ["openai-codex:default"] } },
};
const requests: Array<{ method: string; params: Record<string, unknown> }> = [];
agentRuntimeMocks.ensureAuthProfileStore.mockReturnValue({
version: 1,
@@ -220,6 +225,142 @@ describe("codex conversation binding", () => {
expect(result).toEqual({ handled: true });
});
it("recreates a missing bound thread and preserves auth plus turn overrides", async () => {
const sessionFile = path.join(tempDir, "session.jsonl");
agentRuntimeMocks.ensureAuthProfileStore.mockReturnValue({
version: 1,
profiles: {
work: {
type: "oauth",
provider: "openai-codex",
access: "access-token",
},
},
});
await fs.writeFile(
`${sessionFile}.codex-app-server.json`,
JSON.stringify({
schemaVersion: 1,
threadId: "thread-old",
cwd: tempDir,
authProfileId: "work",
model: "gpt-5.4-mini",
modelProvider: "openai",
approvalPolicy: "on-request",
sandbox: "workspace-write",
serviceTier: "fast",
}),
);
const requests: Array<{ method: string; params: Record<string, unknown> }> = [];
const notificationHandlers: Array<(notification: Record<string, unknown>) => void> = [];
sharedClientMocks.getSharedCodexAppServerClient.mockResolvedValue({
request: vi.fn(async (method: string, requestParams: Record<string, unknown>) => {
requests.push({ method, params: requestParams });
if (method === "turn/start" && requestParams.threadId === "thread-old") {
throw new Error("thread not found: thread-old");
}
if (method === "thread/start") {
return {
thread: { id: "thread-new", cwd: tempDir },
model: "gpt-5.4-mini",
};
}
if (method === "turn/start" && requestParams.threadId === "thread-new") {
setImmediate(() => {
for (const handler of notificationHandlers) {
handler({
method: "turn/completed",
params: {
threadId: "thread-new",
turn: {
id: "turn-new",
status: "completed",
items: [
{
id: "assistant-1",
type: "agentMessage",
text: "Recovered",
},
],
},
},
});
}
});
return { turn: { id: "turn-new" } };
}
throw new Error(`unexpected method: ${method}`);
}),
addNotificationHandler: vi.fn((handler) => {
notificationHandlers.push(handler);
return () => undefined;
}),
addRequestHandler: vi.fn(() => () => undefined),
});
const result = await handleCodexConversationInboundClaim(
{
content: "hi again",
bodyForAgent: "hi again",
channel: "telegram",
isGroup: false,
commandAuthorized: true,
},
{
channelId: "telegram",
pluginBinding: {
bindingId: "binding-1",
pluginId: "codex",
pluginRoot: tempDir,
channel: "telegram",
accountId: "default",
conversationId: "5185575566",
boundAt: Date.now(),
data: {
kind: "codex-app-server-session",
version: 1,
sessionFile,
workspaceDir: tempDir,
},
},
},
{ timeoutMs: 500 },
);
expect(result).toEqual({ handled: true, reply: { text: "Recovered" } });
expect(requests.map((request) => request.method)).toEqual([
"turn/start",
"thread/start",
"turn/start",
]);
expect(sharedClientMocks.getSharedCodexAppServerClient).toHaveBeenCalledWith(
expect.objectContaining({ authProfileId: "work" }),
);
expect(requests[1]?.params).toMatchObject({
model: "gpt-5.4-mini",
approvalPolicy: "on-request",
sandbox: "workspace-write",
serviceTier: "fast",
});
expect(requests[1]?.params).not.toHaveProperty("modelProvider");
expect(requests[2]?.params).toMatchObject({
threadId: "thread-new",
approvalPolicy: "on-request",
serviceTier: "fast",
});
const savedBinding = JSON.parse(
await fs.readFile(`${sessionFile}.codex-app-server.json`, "utf8"),
);
expect(savedBinding).toMatchObject({
threadId: "thread-new",
authProfileId: "work",
approvalPolicy: "on-request",
sandbox: "workspace-write",
serviceTier: "fast",
});
expect(savedBinding).not.toHaveProperty("modelProvider");
});
it("returns a clean failure reply when app-server turn start rejects", async () => {
const sessionFile = path.join(tempDir, "session.jsonl");
await fs.writeFile(

View File

@@ -10,8 +10,11 @@ import { CODEX_CONTROL_METHODS } from "./app-server/capabilities.js";
import {
codexSandboxPolicyForTurn,
resolveCodexAppServerRuntimeOptions,
type CodexAppServerApprovalPolicy,
type CodexAppServerSandboxMode,
} from "./app-server/config.js";
import {
type CodexServiceTier,
type CodexThreadResumeResponse,
type CodexThreadStartResponse,
type CodexTurnStartResponse,
@@ -59,6 +62,9 @@ type CodexConversationStartParams = {
model?: string;
modelProvider?: string;
authProfileId?: string;
approvalPolicy?: CodexAppServerApprovalPolicy;
sandbox?: CodexAppServerSandboxMode;
serviceTier?: CodexServiceTier;
};
type BoundTurnResult = {
@@ -100,6 +106,9 @@ export async function startCodexConversationThread(
model: params.model,
modelProvider: params.modelProvider,
authProfileId,
approvalPolicy: params.approvalPolicy,
sandbox: params.sandbox,
serviceTier: params.serviceTier,
config: params.config,
});
} else {
@@ -110,6 +119,9 @@ export async function startCodexConversationThread(
model: params.model,
modelProvider: params.modelProvider,
authProfileId,
approvalPolicy: params.approvalPolicy,
sandbox: params.sandbox,
serviceTier: params.serviceTier,
config: params.config,
});
}
@@ -137,7 +149,7 @@ export async function handleCodexConversationInboundClaim(
}
try {
const result = await enqueueBoundTurn(data.sessionFile, () =>
runBoundTurn({
runBoundTurnWithMissingThreadRecovery({
data,
prompt,
event,
@@ -177,9 +189,14 @@ async function attachExistingThread(params: {
model?: string;
modelProvider?: string;
authProfileId?: string;
approvalPolicy?: CodexAppServerApprovalPolicy;
sandbox?: CodexAppServerSandboxMode;
serviceTier?: CodexServiceTier;
config?: CodexAppServerAuthProfileLookup["config"];
}): Promise<void> {
const runtime = resolveCodexAppServerRuntimeOptions({ pluginConfig: params.pluginConfig });
const runtime = resolveCodexAppServerRuntimeOptions({
pluginConfig: params.pluginConfig,
});
const modelProvider = resolveThreadRequestModelProvider({
authProfileId: params.authProfileId,
modelProvider: params.modelProvider,
@@ -196,10 +213,12 @@ async function attachExistingThread(params: {
threadId: params.threadId,
...(params.model ? { model: params.model } : {}),
...(modelProvider ? { modelProvider } : {}),
approvalPolicy: runtime.approvalPolicy,
approvalPolicy: params.approvalPolicy ?? runtime.approvalPolicy,
approvalsReviewer: runtime.approvalsReviewer,
sandbox: runtime.sandbox,
...(runtime.serviceTier ? { serviceTier: runtime.serviceTier } : {}),
sandbox: params.sandbox ?? runtime.sandbox,
...((params.serviceTier ?? runtime.serviceTier)
? { serviceTier: params.serviceTier ?? runtime.serviceTier }
: {}),
persistExtendedHistory: true,
},
{ timeoutMs: runtime.requestTimeoutMs },
@@ -217,9 +236,9 @@ async function attachExistingThread(params: {
authProfileId: params.authProfileId,
modelProvider: response.modelProvider ?? params.modelProvider,
}),
approvalPolicy: runtime.approvalPolicy,
sandbox: runtime.sandbox,
serviceTier: runtime.serviceTier,
approvalPolicy: params.approvalPolicy ?? runtime.approvalPolicy,
sandbox: params.sandbox ?? runtime.sandbox,
serviceTier: params.serviceTier ?? runtime.serviceTier,
},
{
config: params.config,
@@ -234,9 +253,14 @@ async function createThread(params: {
model?: string;
modelProvider?: string;
authProfileId?: string;
approvalPolicy?: CodexAppServerApprovalPolicy;
sandbox?: CodexAppServerSandboxMode;
serviceTier?: CodexServiceTier;
config?: CodexAppServerAuthProfileLookup["config"];
}): Promise<void> {
const runtime = resolveCodexAppServerRuntimeOptions({ pluginConfig: params.pluginConfig });
const runtime = resolveCodexAppServerRuntimeOptions({
pluginConfig: params.pluginConfig,
});
const modelProvider = resolveThreadRequestModelProvider({
authProfileId: params.authProfileId,
modelProvider: params.modelProvider,
@@ -253,10 +277,12 @@ async function createThread(params: {
cwd: params.workspaceDir,
...(params.model ? { model: params.model } : {}),
...(modelProvider ? { modelProvider } : {}),
approvalPolicy: runtime.approvalPolicy,
approvalPolicy: params.approvalPolicy ?? runtime.approvalPolicy,
approvalsReviewer: runtime.approvalsReviewer,
sandbox: runtime.sandbox,
...(runtime.serviceTier ? { serviceTier: runtime.serviceTier } : {}),
sandbox: params.sandbox ?? runtime.sandbox,
...((params.serviceTier ?? runtime.serviceTier)
? { serviceTier: params.serviceTier ?? runtime.serviceTier }
: {}),
developerInstructions:
"This Codex thread is bound to an OpenClaw conversation. Answer normally; OpenClaw will deliver your final response back to the conversation.",
experimentalRawEvents: true,
@@ -276,9 +302,9 @@ async function createThread(params: {
authProfileId: params.authProfileId,
modelProvider: response.modelProvider ?? params.modelProvider,
}),
approvalPolicy: runtime.approvalPolicy,
sandbox: runtime.sandbox,
serviceTier: runtime.serviceTier,
approvalPolicy: params.approvalPolicy ?? runtime.approvalPolicy,
sandbox: params.sandbox ?? runtime.sandbox,
serviceTier: params.serviceTier ?? runtime.serviceTier,
},
{
config: params.config,
@@ -293,7 +319,9 @@ async function runBoundTurn(params: {
pluginConfig?: unknown;
timeoutMs?: number;
}): Promise<BoundTurnResult> {
const runtime = resolveCodexAppServerRuntimeOptions({ pluginConfig: params.pluginConfig });
const runtime = resolveCodexAppServerRuntimeOptions({
pluginConfig: params.pluginConfig,
});
const binding = await readCodexAppServerBinding(params.data.sessionFile);
const threadId = binding?.threadId;
if (!threadId) {
@@ -350,7 +378,10 @@ async function runBoundTurn(params: {
"turn/start",
{
threadId,
input: buildCodexConversationTurnInput({ prompt: params.prompt, event: params.event }),
input: buildCodexConversationTurnInput({
prompt: params.prompt,
event: params.event,
}),
cwd: binding.cwd || params.data.workspaceDir,
approvalPolicy: binding.approvalPolicy ?? runtime.approvalPolicy,
approvalsReviewer: runtime.approvalsReviewer,
@@ -389,6 +420,39 @@ async function runBoundTurn(params: {
}
}
async function runBoundTurnWithMissingThreadRecovery(params: {
data: CodexConversationBindingData;
prompt: string;
event: PluginHookInboundClaimEvent;
pluginConfig?: unknown;
timeoutMs?: number;
}): Promise<BoundTurnResult> {
try {
return await runBoundTurn(params);
} catch (error) {
if (!isCodexThreadNotFoundError(error)) {
throw error;
}
const binding = await readCodexAppServerBinding(params.data.sessionFile);
await startCodexConversationThread({
pluginConfig: params.pluginConfig,
sessionFile: params.data.sessionFile,
workspaceDir: binding?.cwd || params.data.workspaceDir,
model: binding?.model,
modelProvider: binding?.modelProvider,
authProfileId: binding?.authProfileId,
approvalPolicy: binding?.approvalPolicy,
sandbox: binding?.sandbox,
serviceTier: binding?.serviceTier,
});
return await runBoundTurn(params);
}
}
function isCodexThreadNotFoundError(error: unknown): boolean {
return /\bthread not found:/iu.test(formatErrorMessage(error));
}
function enqueueBoundTurn<T>(key: string, run: () => Promise<T>): Promise<T> {
const state = getGlobalState();
const previous = state.queues.get(key) ?? Promise.resolve();

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/comfy-provider",
"version": "2026.5.4",
"version": "2026.5.7",
"private": true,
"description": "OpenClaw ComfyUI provider plugin",
"type": "module",

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/deepgram-provider",
"version": "2026.5.4",
"version": "2026.5.7",
"private": true,
"description": "OpenClaw Deepgram media-understanding provider",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/deepinfra-provider",
"version": "2026.5.4",
"version": "2026.5.7",
"private": true,
"description": "OpenClaw DeepInfra provider plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/deepseek-provider",
"version": "2026.5.4",
"version": "2026.5.7",
"private": true,
"description": "OpenClaw DeepSeek provider plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/diagnostics-otel",
"version": "2026.5.4",
"version": "2026.5.7",
"description": "OpenClaw diagnostics OpenTelemetry exporter",
"repository": {
"type": "git",
@@ -34,10 +34,10 @@
"minHostVersion": ">=2026.4.25"
},
"compat": {
"pluginApi": ">=2026.5.4"
"pluginApi": ">=2026.5.7"
},
"build": {
"openclawVersion": "2026.5.4"
"openclawVersion": "2026.5.7"
},
"release": {
"publishToClawHub": true,

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/diagnostics-prometheus",
"version": "2026.5.4",
"version": "2026.5.7",
"description": "OpenClaw diagnostics Prometheus exporter",
"repository": {
"type": "git",
@@ -21,10 +21,10 @@
"minHostVersion": ">=2026.4.25"
},
"compat": {
"pluginApi": ">=2026.5.4"
"pluginApi": ">=2026.5.7"
},
"build": {
"openclawVersion": "2026.5.4"
"openclawVersion": "2026.5.7"
},
"release": {
"publishToClawHub": true,

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/diffs",
"version": "2026.5.4",
"version": "2026.5.7",
"description": "OpenClaw diff viewer plugin",
"repository": {
"type": "git",
@@ -30,10 +30,10 @@
"minHostVersion": ">=2026.4.30"
},
"compat": {
"pluginApi": ">=2026.5.4"
"pluginApi": ">=2026.5.7"
},
"build": {
"openclawVersion": "2026.5.4",
"openclawVersion": "2026.5.7",
"staticAssets": [
{
"source": "./assets/viewer-runtime.js",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/discord",
"version": "2026.5.4",
"version": "2026.5.7",
"description": "OpenClaw Discord channel plugin",
"repository": {
"type": "git",
@@ -21,7 +21,7 @@
"openclaw": "workspace:*"
},
"peerDependencies": {
"openclaw": ">=2026.5.4"
"openclaw": ">=2026.5.7"
},
"peerDependenciesMeta": {
"openclaw": {
@@ -65,10 +65,10 @@
"allowInvalidConfigRecovery": true
},
"compat": {
"pluginApi": ">=2026.5.4"
"pluginApi": ">=2026.5.7"
},
"build": {
"openclawVersion": "2026.5.4"
"openclawVersion": "2026.5.7"
},
"release": {
"publishToClawHub": true,

View File

@@ -1,3 +1,4 @@
import { ChannelType } from "discord-api-types/v10";
import type {
DiscordGuildChannelConfig,
DiscordGuildEntry,
@@ -23,7 +24,21 @@ export type DiscordChannelPermissionsAudit = {
elapsedMs: number;
};
const REQUIRED_CHANNEL_PERMISSIONS = ["ViewChannel", "SendMessages"] as const;
const REQUIRED_TEXT_CHANNEL_PERMISSIONS = ["ViewChannel", "SendMessages"] as const;
const REQUIRED_VOICE_CHANNEL_PERMISSIONS = [
"ViewChannel",
"Connect",
"Speak",
"SendMessages",
"ReadMessageHistory",
] as const;
export function resolveRequiredDiscordChannelPermissions(channelType?: number): string[] {
if (channelType === ChannelType.GuildVoice || channelType === ChannelType.GuildStageVoice) {
return [...REQUIRED_VOICE_CHANNEL_PERMISSIONS];
}
return [...REQUIRED_TEXT_CHANNEL_PERMISSIONS];
}
function shouldAuditChannelConfig(config: DiscordGuildChannelConfig | undefined) {
if (!config) {
@@ -76,6 +91,27 @@ export function collectDiscordAuditChannelIdsForGuilds(
return { channelIds, unresolvedChannels };
}
export function collectDiscordAuditChannelIdsForAccount(config: {
guilds?: Record<string, DiscordGuildEntry>;
voice?: { autoJoin?: Array<{ guildId?: string; channelId?: string }> };
}) {
const collected = collectDiscordAuditChannelIdsForGuilds(config.guilds);
const channelIds = new Set(collected.channelIds);
let unresolvedVoiceChannels = 0;
for (const entry of config.voice?.autoJoin ?? []) {
const channelId = normalizeOptionalString(entry?.channelId) ?? "";
if (/^\d+$/.test(channelId)) {
channelIds.add(channelId);
} else if (channelId) {
unresolvedVoiceChannels++;
}
}
return {
channelIds: [...channelIds].toSorted((a, b) => a.localeCompare(b)),
unresolvedChannels: collected.unresolvedChannels + unresolvedVoiceChannels,
};
}
export async function auditDiscordChannelPermissionsWithFetcher(params: {
cfg: OpenClawConfig;
token: string;
@@ -87,6 +123,7 @@ export async function auditDiscordChannelPermissionsWithFetcher(params: {
params: { cfg: OpenClawConfig; token: string; accountId?: string },
) => Promise<{
permissions: string[];
channelType?: number;
}>;
}): Promise<DiscordChannelPermissionsAudit> {
const started = Date.now();
@@ -101,7 +138,6 @@ export async function auditDiscordChannelPermissionsWithFetcher(params: {
};
}
const required = [...REQUIRED_CHANNEL_PERMISSIONS];
const channels: DiscordChannelPermissionsAuditEntry[] = [];
for (const channelId of params.channelIds) {
@@ -111,6 +147,7 @@ export async function auditDiscordChannelPermissionsWithFetcher(params: {
token,
accountId: params.accountId ?? undefined,
});
const required = resolveRequiredDiscordChannelPermissions(perms.channelType);
const missing = required.filter((p) => !perms.permissions.includes(p));
channels.push({
channelId,

View File

@@ -1,7 +1,9 @@
import { ChannelType } from "discord-api-types/v10";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
import { beforeEach, describe, expect, it, vi } from "vitest";
import {
auditDiscordChannelPermissionsWithFetcher,
collectDiscordAuditChannelIdsForAccount,
collectDiscordAuditChannelIdsForGuilds,
} from "./audit-core.js";
@@ -142,4 +144,59 @@ describe("discord audit", () => {
expect(collected.channelIds).toEqual(["111"]);
expect(collected.unresolvedChannels).toBe(1);
});
it("includes configured voice auto-join channels in permission audits", () => {
const collected = collectDiscordAuditChannelIdsForAccount({
guilds: {
"123": {
channels: {
"111": { enabled: true },
},
},
},
voice: {
autoJoin: [
{ guildId: "123", channelId: "222" },
{ guildId: "123", channelId: "general" },
],
},
});
expect(collected.channelIds).toEqual(["111", "222"]);
expect(collected.unresolvedChannels).toBe(1);
});
it.each([ChannelType.GuildVoice, ChannelType.GuildStageVoice])(
"requires voice permissions for voice channel audit targets of type %s",
async (channelType) => {
const cfg = {
channels: {
discord: {
enabled: true,
token: "t",
},
},
} as unknown as OpenClawConfig;
fetchChannelPermissionsDiscordMock.mockResolvedValueOnce({
channelId: "222",
permissions: ["ViewChannel", "SendMessages"],
channelType,
raw: "0",
isDm: false,
});
const audit = await auditDiscordChannelPermissionsWithFetcher({
cfg,
token: "t",
accountId: "default",
channelIds: ["222"],
timeoutMs: 1000,
fetchChannelPermissions: fetchChannelPermissionsDiscordMock,
});
expect(audit.ok).toBe(false);
expect(audit.channels[0]?.missing).toEqual(["Connect", "Speak", "ReadMessageHistory"]);
},
);
});

View File

@@ -2,7 +2,7 @@ import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
import { inspectDiscordAccount } from "./account-inspect.js";
import {
auditDiscordChannelPermissionsWithFetcher,
collectDiscordAuditChannelIdsForGuilds,
collectDiscordAuditChannelIdsForAccount,
type DiscordChannelPermissionsAudit,
} from "./audit-core.js";
import { fetchChannelPermissionsDiscord } from "./send.js";
@@ -15,7 +15,7 @@ export function collectDiscordAuditChannelIds(params: {
cfg: params.cfg,
accountId: params.accountId,
});
return collectDiscordAuditChannelIdsForGuilds(account.config.guilds);
return collectDiscordAuditChannelIdsForAccount(account.config);
}
export async function auditDiscordChannelPermissions(params: {

View File

@@ -1,5 +1,6 @@
import { readFile } from "node:fs/promises";
import { resolve } from "node:path";
import { ChannelType } from "discord-api-types/v10";
import { createStartAccountContext } from "openclaw/plugin-sdk/channel-test-helpers";
import type { PluginRuntime } from "openclaw/plugin-sdk/core";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
@@ -163,6 +164,33 @@ describe("discordPlugin outbound", () => {
});
});
it("resolves bare allowlisted Discord user IDs as message-tool DM targets", async () => {
const resolveTarget = discordPlugin.messaging?.targetResolver?.resolveTarget;
if (!resolveTarget) {
throw new Error(
"Expected discordPlugin.messaging.targetResolver.resolveTarget to be defined",
);
}
await expect(
resolveTarget({
cfg: {
channels: {
discord: {
allowFrom: ["1439091261670948987"],
},
},
} as OpenClawConfig,
input: "1439091261670948987",
normalized: "channel:1439091261670948987",
preferredKind: "channel",
}),
).resolves.toMatchObject({
to: "user:1439091261670948987",
kind: "user",
});
});
it("honors per-account replyToMode overrides", () => {
const resolveReplyToMode = discordPlugin.threading?.resolveReplyToMode;
if (!resolveReplyToMode) {
@@ -379,6 +407,42 @@ describe("discordPlugin outbound", () => {
expect(runtimeProbeDiscord).not.toHaveBeenCalled();
});
it("reports missing voice permissions in targeted capabilities diagnostics", async () => {
const fetchPermissionsSpy = vi
.spyOn(sendModule, "fetchChannelPermissionsDiscord")
.mockResolvedValueOnce({
channelId: "222",
guildId: "123",
permissions: ["ViewChannel", "SendMessages"],
raw: "0",
isDm: false,
channelType: ChannelType.GuildVoice,
});
try {
const cfg = createCfg();
const diagnostics = await discordPlugin.status!.buildCapabilitiesDiagnostics!({
account: resolveAccount(cfg),
timeoutMs: 5000,
cfg,
target: "channel:222",
});
expect(fetchPermissionsSpy).toHaveBeenCalledWith(
"222",
expect.objectContaining({ token: "discord-token" }),
);
expect(diagnostics?.details?.permissions).toMatchObject({
channelId: "222",
missingRequired: ["Connect", "Speak", "ReadMessageHistory"],
});
expect(diagnostics?.lines?.map((line) => line.text).join("\n")).toContain(
"Missing required: Connect, Speak, ReadMessageHistory",
);
} finally {
fetchPermissionsSpy.mockRestore();
}
});
it("uses direct Discord startup helpers for async startup enrichment", async () => {
const runtimeProbeDiscord = vi.fn(async () => {
throw new Error("runtime Discord probe should not be used");

View File

@@ -28,6 +28,7 @@ import {
type ResolvedDiscordAccount,
} from "./accounts.js";
import { getDiscordApprovalCapability } from "./approval-native.js";
import { resolveRequiredDiscordChannelPermissions } from "./audit-core.js";
import { discordMessageActions as discordMessageActionsImpl } from "./channel-actions.js";
import {
buildTokenChannelStatusSummary,
@@ -78,8 +79,8 @@ import { discordSetupAdapter } from "./setup-adapter.js";
import { createDiscordPluginBase, discordConfigAdapter } from "./shared.js";
import { collectDiscordStatusIssues } from "./status-issues.js";
import { parseDiscordTarget } from "./target-parsing.js";
import { resolveDiscordTarget } from "./target-resolver.js";
const REQUIRED_DISCORD_PERMISSIONS = ["ViewChannel", "SendMessages"] as const;
const DISCORD_ACCOUNT_STARTUP_STAGGER_MS = 10_000;
function startDiscordStartupProbe(params: {
@@ -301,6 +302,21 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount, DiscordProbe>
targetResolver: {
looksLikeId: looksLikeDiscordTargetId,
hint: "<channelId|user:ID|channel:ID>",
resolveTarget: async ({ cfg, accountId, input, preferredKind }) => {
const target = await resolveDiscordTarget(
input,
{ cfg, accountId: accountId ?? undefined },
{ defaultKind: preferredKind === "user" ? "user" : "channel" },
);
return target
? {
to: target.normalized,
kind: target.kind,
display: target.raw,
source: "normalized",
}
: null;
},
},
},
approvalCapability: getDiscordApprovalCapability(),
@@ -513,7 +529,8 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount, DiscordProbe>
token,
accountId: account.accountId ?? undefined,
});
const missingRequired = REQUIRED_DISCORD_PERMISSIONS.filter(
const requiredPermissions = resolveRequiredDiscordChannelPermissions(perms.channelType);
const missingRequired = requiredPermissions.filter(
(permission) => !perms.permissions.includes(permission),
);
details.permissions = {

View File

@@ -152,11 +152,13 @@ describe("discord config schema", () => {
voice: {
connectTimeoutMs: 45_000,
reconnectGraceMs: 20_000,
captureSilenceGraceMs: 3_500,
},
});
expect(cfg.voice?.connectTimeoutMs).toBe(45_000);
expect(cfg.voice?.reconnectGraceMs).toBe(20_000);
expect(cfg.voice?.captureSilenceGraceMs).toBe(3_500);
});
it("rejects invalid Discord voice timing overrides", () => {
@@ -165,6 +167,8 @@ describe("discord config schema", () => {
{ connectTimeoutMs: 120_001 },
{ reconnectGraceMs: -1 },
{ reconnectGraceMs: 1.5 },
{ captureSilenceGraceMs: 0 },
{ captureSilenceGraceMs: 30_001 },
]) {
expectInvalidDiscordConfig({ voice });
}

View File

@@ -201,6 +201,10 @@ export const discordChannelConfigUiHints = {
label: "Discord Voice Reconnect Grace (ms)",
help: "Grace period for a disconnected Discord voice session to enter Signalling or Connecting before OpenClaw destroys it. Default: 15000.",
},
"voice.captureSilenceGraceMs": {
label: "Discord Voice Capture Silence Grace (ms)",
help: "Silence window after Discord reports a speaker ended before OpenClaw finalizes the audio segment for transcription. Default: 2500.",
},
"voice.tts": {
label: "Discord Voice Text-to-Speech",
help: "Optional TTS overrides for Discord voice playback (merged with messages.tts).",

View File

@@ -0,0 +1,114 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { GatewayHeartbeatTimers } from "./gateway-lifecycle.js";
describe("GatewayHeartbeatTimers", () => {
afterEach(() => {
vi.restoreAllMocks();
vi.useRealTimers();
});
it("does not false-timeout when the first heartbeat fires near the interval boundary", () => {
vi.useFakeTimers();
const onHeartbeat = vi.fn();
const onAckTimeout = vi.fn();
const isAcked = vi.fn().mockReturnValue(false);
const timers = new GatewayHeartbeatTimers();
timers.start({
intervalMs: 45_000,
isAcked,
onAckTimeout,
onHeartbeat,
random: () => 0.95,
});
vi.advanceTimersByTime(42_750);
expect(onHeartbeat).toHaveBeenCalledTimes(1);
expect(onAckTimeout).not.toHaveBeenCalled();
vi.advanceTimersByTime(2_250);
expect(onAckTimeout).not.toHaveBeenCalled();
isAcked.mockReturnValue(true);
vi.advanceTimersByTime(42_750);
expect(onHeartbeat).toHaveBeenCalledTimes(2);
expect(onAckTimeout).not.toHaveBeenCalled();
timers.stop();
});
it("fires an ACK timeout when a heartbeat is genuinely not acknowledged", () => {
vi.useFakeTimers();
const timers = new GatewayHeartbeatTimers();
const onHeartbeat = vi.fn();
const onAckTimeout = vi.fn();
timers.start({
intervalMs: 45_000,
isAcked: () => false,
onAckTimeout,
onHeartbeat,
random: () => 0,
});
vi.advanceTimersByTime(0);
expect(onHeartbeat).toHaveBeenCalledTimes(1);
vi.advanceTimersByTime(45_000);
expect(onAckTimeout).toHaveBeenCalledTimes(1);
timers.stop();
});
it("sends heartbeats at regular intervals after the initial random delay", () => {
vi.useFakeTimers();
const timers = new GatewayHeartbeatTimers();
const onHeartbeat = vi.fn();
const onAckTimeout = vi.fn();
timers.start({
intervalMs: 10_000,
isAcked: () => true,
onAckTimeout,
onHeartbeat,
random: () => 0.5,
});
vi.advanceTimersByTime(5_000);
expect(onHeartbeat).toHaveBeenCalledTimes(1);
vi.advanceTimersByTime(10_000);
expect(onHeartbeat).toHaveBeenCalledTimes(2);
vi.advanceTimersByTime(10_000);
expect(onHeartbeat).toHaveBeenCalledTimes(3);
expect(onAckTimeout).not.toHaveBeenCalled();
timers.stop();
});
it("stop cancels all pending timers", () => {
vi.useFakeTimers();
const timers = new GatewayHeartbeatTimers();
const onHeartbeat = vi.fn();
const onAckTimeout = vi.fn();
timers.start({
intervalMs: 10_000,
isAcked: () => true,
onAckTimeout,
onHeartbeat,
random: () => 0.5,
});
timers.stop();
vi.advanceTimersByTime(100_000);
expect(onHeartbeat).not.toHaveBeenCalled();
expect(onAckTimeout).not.toHaveBeenCalled();
});
});

View File

@@ -4,6 +4,24 @@ export class GatewayHeartbeatTimers {
heartbeatInterval?: GatewayTimer;
firstHeartbeatTimeout?: GatewayTimer;
private scheduleHeartbeatCycle(params: {
intervalMs: number;
isAcked: () => boolean;
onAckTimeout: () => void;
onHeartbeat: () => void;
}): void {
this.heartbeatInterval = setTimeout(() => {
this.heartbeatInterval = undefined;
if (!params.isAcked()) {
params.onAckTimeout();
return;
}
params.onHeartbeat();
this.scheduleHeartbeatCycle(params);
}, params.intervalMs);
this.heartbeatInterval.unref?.();
}
start(params: {
intervalMs: number;
isAcked: () => boolean;
@@ -14,23 +32,19 @@ export class GatewayHeartbeatTimers {
this.stop();
const random = params.random ?? Math.random;
this.firstHeartbeatTimeout = setTimeout(
params.onHeartbeat,
() => {
this.firstHeartbeatTimeout = undefined;
params.onHeartbeat();
this.scheduleHeartbeatCycle(params);
},
Math.max(0, params.intervalMs * random()),
);
this.firstHeartbeatTimeout.unref?.();
this.heartbeatInterval = setInterval(() => {
if (!params.isAcked()) {
params.onAckTimeout();
return;
}
params.onHeartbeat();
}, params.intervalMs);
this.heartbeatInterval.unref?.();
}
stop(): void {
if (this.heartbeatInterval) {
clearInterval(this.heartbeatInterval);
clearTimeout(this.heartbeatInterval);
this.heartbeatInterval = undefined;
}
if (this.firstHeartbeatTimeout) {

View File

@@ -82,6 +82,8 @@ export function createDiscordDraftPreviewController(params: {
});
let previewToolProgressSuppressed = false;
let previewToolProgressLines: string[] = [];
let reasoningProgressRawText = "";
let lastReasoningProgressLine: string | undefined;
const progressSeed = `${params.accountId}:${params.deliverChannelId}`;
const renderProgressDraft = async (options?: { flush?: boolean }) => {
@@ -116,6 +118,8 @@ export function createDiscordDraftPreviewController(params: {
draftChunker?.reset();
previewToolProgressSuppressed = false;
previewToolProgressLines = [];
reasoningProgressRawText = "";
lastReasoningProgressLine = undefined;
};
const forceNewMessageIfNeeded = () => {
@@ -163,8 +167,11 @@ export function createDiscordDraftPreviewController(params: {
return;
}
const normalized = line?.replace(/\s+/g, " ").trim();
if (!normalized) {
return;
}
if (discordStreamMode !== "progress") {
if (!previewToolProgressEnabled || previewToolProgressSuppressed || !normalized) {
if (!previewToolProgressEnabled || previewToolProgressSuppressed) {
return;
}
const previous = previewToolProgressLines.at(-1);
@@ -200,6 +207,36 @@ export function createDiscordDraftPreviewController(params: {
await renderProgressDraft();
}
},
async pushReasoningProgress(text?: string) {
if (!draftStream || discordStreamMode !== "progress" || !text) {
return;
}
reasoningProgressRawText = mergeReasoningProgressText(reasoningProgressRawText, text);
const normalized = normalizeReasoningProgressLine(reasoningProgressRawText);
if (!normalized) {
return;
}
if (previewToolProgressEnabled && !previewToolProgressSuppressed) {
const priorIndex =
lastReasoningProgressLine === undefined
? -1
: previewToolProgressLines.lastIndexOf(lastReasoningProgressLine);
if (priorIndex >= 0) {
previewToolProgressLines = [...previewToolProgressLines];
previewToolProgressLines[priorIndex] = normalized;
} else {
previewToolProgressLines = [...previewToolProgressLines, normalized].slice(
-resolveChannelProgressDraftMaxLines(params.discordConfig),
);
}
lastReasoningProgressLine = normalized;
}
const alreadyStarted = progressDraftGate.hasStarted;
await progressDraftGate.noteWork();
if (alreadyStarted && progressDraftGate.hasStarted) {
await renderProgressDraft();
}
},
resolvePreviewFinalText(text?: string) {
if (typeof text !== "string") {
return undefined;
@@ -329,3 +366,29 @@ export function createDiscordDraftPreviewController(params: {
},
};
}
function normalizeReasoningProgressLine(text: string): string {
return text
.replace(/^\s*(?:>\s*)?Reasoning:\s*/i, "")
.replace(/\s+/g, " ")
.trim();
}
function mergeReasoningProgressText(current: string, incoming: string): string {
if (!current) {
return incoming;
}
const normalizedCurrent = normalizeReasoningProgressLine(current);
const normalizedIncoming = normalizeReasoningProgressLine(incoming);
if (!normalizedIncoming || normalizedIncoming === normalizedCurrent) {
return current;
}
if (isReasoningSnapshotText(incoming) || normalizedIncoming.startsWith(normalizedCurrent)) {
return incoming;
}
return `${current}${incoming}`;
}
function isReasoningSnapshotText(text: string): boolean {
return /^\s*(?:>\s*)?Reasoning:\s*/i.test(text);
}

View File

@@ -65,9 +65,11 @@ export function createDiscordMessage(params: {
mentionedEveryone?: boolean;
attachments?: Array<Record<string, unknown>>;
webhookId?: string;
type?: import("../internal/discord.js").MessageType;
}): import("../internal/discord.js").Message {
return {
id: params.id,
type: params.type,
content: params.content,
timestamp: new Date().toISOString(),
channelId: params.channelId,

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