Compare commits

..

763 Commits

Author SHA1 Message Date
Alex Knight
66354a4258 fix: preserve codex preflight compaction route 2026-05-25 12:21:54 +10:00
Vincent Koc
4798264a29 fix(e2e): harden bundled lifecycle probe on Windows 2026-05-25 04:14:00 +02:00
Vincent Koc
60c0f249ad test(e2e): sample kitchen sink rpc peak rss 2026-05-25 03:50:01 +02:00
Vincent Koc
ea3bb9282c fix(scripts): remove stale deadcode allowlist entries 2026-05-25 03:40:29 +02:00
Galin Iliev
b5c1199217 fix(telegram): route polling diagnostics away from errors
Route normal [telegram][diag] polling diagnostics through runtime.log while keeping non-diag Telegram warnings/errors and offset persistence failures on runtime.error.

Verification:
- node scripts/run-vitest.mjs extensions/telegram/src/monitor.test.ts (34 passed)
- git diff --check
- CI run 26378692736 passed on 979c6f31a4

Fixes #82957
2026-05-24 18:39:52 -07:00
Vincent Koc
793e300cc5 fix(plugins): support linked source checkouts on Windows 2026-05-25 03:36:06 +02:00
Galin Iliev
42bdc949f2 fix(gateway): back off session tool mirrors under pressure (#84846)
Co-authored-by: Galin Iliev <Galin.Iliev@microsoft.com>
2026-05-24 18:34:37 -07:00
Gio Della-Libera
06bf302864 fix(config): skip shell env fallback on Windows (#85739) 2026-05-24 18:27:08 -07:00
Galin Iliev
14590445a6 fix(gateway): avoid duplicate session message broadcasts 2026-05-24 18:26:42 -07:00
Omar Shahine
f37fbc9ef4 fix: repair anchorless iMessage watch payloads
Repair explicit anchorless iMessage watch payloads by GUID before debounce/routing, and drop unrecoverable payloads fail-closed instead of routing them as sender DMs.

Closes #84470.
Refs #84503.

Thanks @zhangguiping-xydt and @zqchris.
2026-05-24 18:13:03 -07:00
Gio Della-Libera
749692ec37 fix(cli): route node status hints to stdout (#85780) 2026-05-24 18:11:38 -07:00
Gio Della-Libera
3a72a30074 fix(oc-path): support deep config edits (#86060) 2026-05-24 18:10:02 -07:00
Vincent Koc
f3f4f29dba fix(config): quiet benign metadata anomaly output 2026-05-25 03:06:28 +02:00
Vincent Koc
732cf54240 fix(test): fail multi-node update regressions 2026-05-25 02:44:02 +02:00
Damian Finol
f09b4ebe31 fix(google-vertex): support production ADC modes (#83971)
Fix Google Vertex production ADC mode support by routing explicit google-vertex models to the Vertex transport and relying on google-auth-library for request-time ADC resolution.

Verification:
- pnpm install --frozen-lockfile
- pnpm test extensions/google/transport-stream.test.ts extensions/google/index.test.ts src/config/zod-schema.models.test.ts src/agents/pi-embedded-runner/model.inline-provider.test.ts -- --reporter=verbose
- pnpm check:changed
- GitHub PR checks green on c4b7cad4df
- Live ADC smoke reached Google Vertex auth/transport and failed only because the configured redacted project has the Vertex AI API disabled

Co-authored-by: Damian Finol <damian@felixpago.com>
2026-05-25 01:37:52 +01:00
Vincent Koc
fa3ff4d503 test(e2e): expose corrupt plugin deps smoke 2026-05-25 02:27:53 +02:00
Peter Steinberger
d9af23fb5a fix(codex): log app-server approval promotion trigger 2026-05-25 01:26:37 +01:00
Vincent Koc
ec168fa2bd test(e2e): harden multi-node update smoke 2026-05-25 01:59:32 +02:00
Andy Ye
8dc6b4d330 Clean up browser MCP subprocess tree (#85832)
* fix: clean up browser MCP subprocess tree

* fix: clean up windows browser mcp tree before close

* fix(browser): repair chrome mcp cleanup rebase

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-05-25 00:57:34 +01:00
Sebastien Tardif
907bc0371c fix(agents): log warnings instead of swallowing subagent errors (#82943)
* fix: log subagent swallowed errors in hook emission and restore paths

Wire createSubsystemLogger into the two silent catch blocks that
discard errors during subagent lifecycle:

1. emitSubagentEndedHookOnce (subagent-registry-completion.ts):
   catch { return false } -> catch (err) { log.warn(...); return false }

2. restoreSubagentRunsOnce (subagent-registry.ts):
   catch { /* ignore */ } -> catch (err) { log.warn(...) }

Both paths now log the error message before continuing, providing
a diagnostic trail when hook emission or disk restore fails silently.

Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>

* test(agents): keep provider test mocks current

---------

Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-05-25 00:57:29 +01:00
Sebastien Tardif
f0061ddc54 fix(compaction): preserve partial summary on mid-chain chunk failure (#82952)
* fix(compaction): preserve partial summary on mid-chain chunk failure

When summarizing multiple chunks, if a chunk fails after at least one
chunk has already succeeded, return the partial summary instead of
propagating the error and losing all summarization progress.

Abort and timeout errors still propagate immediately. First-chunk
failures still rethrow so the existing fallback path runs.

Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>

* fix(compaction): use content array for assistant messages to match updated AgentMessage type

* fix(compaction): use as-unknown-as-AgentMessage cast for assistant test fixtures

---------

Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
2026-05-25 00:57:25 +01:00
Sebastien Tardif
5d174a5bec fix(config): do not suppress recovery retry after failed backup restore (#85787)
maybeRecoverSuspiciousConfigRead unconditionally recorded
lastObservedSuspiciousSignature in health state even when
restoredFromBackup was false (copyFile failed). The guard at
resolveConfigReadRecoveryContext then prevented the same
signature from ever being retried, permanently accepting the
suspicious config on every subsequent launch.

Only record the dedup signature when the backup restore
actually succeeded.
2026-05-25 00:57:21 +01:00
Peter Steinberger
c422e7240f chore: release 2026.5.25 2026-05-25 00:46:47 +01:00
Vincent Koc
f68ed721b1 fix(installer): support alpine cli installs 2026-05-25 01:28:13 +02:00
Vincent Koc
2a73725b5d test(agents): keep runtime-plan provider mock current 2026-05-25 01:16:28 +02:00
Vincent Koc
4d4ce9e2f3 fix(scripts): launch env package scripts on Windows 2026-05-25 01:15:49 +02:00
Vincent Koc
3c8d101f5a fix(agents): cache fallback provider resolution 2026-05-25 00:55:30 +02:00
Vincent Koc
8ae997749d fix(test): make import timing scripts Windows-safe 2026-05-25 00:35:04 +02:00
Dallin Romney
8209426867 fix(telegram): transient Telegram pairing prompts (#85555)
* fix: avoid false telegram pairing prompts

* docs: add telegram pairing changelog

* refactor(telegram): share pairing-store gating and align isGroup check

Extract loadTelegramPairingStoreIfNeeded so the text-fragment flush path
and resolveTelegramGroupAllowFromContext share one implementation, and
align the isGroup derivation in the flush path with the
'group || supergroup' form used elsewhere in bot-handlers.runtime.ts.

Note on transient-vs-known errors: readChannelAllowFromStore already
translates missing-file (ENOENT) and JSON parse failures to an empty
allowlist internally, so the only errors that escape into the new
silent-drop path are unexpected I/O failures (EMFILE/EACCES/EIO/...) —
unpaired senders still get a pairing challenge as expected.

* fix(telegram): skip pairing-store read when commands.allowFrom already authorizes the sender

Native command auth resolves group/dm allow context (which may read the
pairing store) before checking commands.allowFrom. On DMs with
dmPolicy: "pairing", a transient pairing-store I/O failure was therefore
dropping commands from senders explicitly authorized by
commands.allowFrom.telegram.

Add a skipPairingStoreRead hint on resolveTelegramGroupAllowFromContext /
loadTelegramPairingStoreIfNeeded, precompute the command authorization
once at chat scope before the context call, and pass the hint when that
pre-check already authorizes the sender. The post-context command auth
check still owns the topic-scoped decision.

Regression covers a DM /status from a sender allowed by
commands.allowFrom.telegram with dmPolicy: "pairing" and a rejecting
readChannelAllowFromStore mock.

* fix(telegram): satisfy test-types on harness readChannelAllowFromStore

CI check-test-types failed because the harness now stores a loose
AnyAsyncMock for readChannelAllowFromStore but TelegramNativeCommandDeps
requires the precise typeof readChannelAllowFromStore signature. Cast at
the telegramDeps assignment so harness callers can keep passing any
vi.fn(...) (including ones that reject) without type pollution at the
call site.

* feat(telegram): reply with a retry hint when pairing-store read fails transiently

Wrap unexpected pairing-store I/O errors (EACCES, EMFILE, ...) in a
typed TelegramPairingStoreReadError and surface them through
handleInboundMessageLike with a friendly "please try again" reply that
matches the media-failure precedent at bot-handlers.runtime.ts:1893.
Beats silent drop: paired senders see why their message wasn't
processed, and unpaired senders who happen to send a DM during a
transient store outage retry naturally and get the correct pairing
prompt once the store recovers.

Verified live against @paxicoto_bot with chmod 000 on
~/.openclaw/credentials/telegram-default-allowFrom.json after touching
mtime to bypass the stat-pinned cache.
2026-05-24 15:12:30 -07:00
Vincent Koc
b681d5d5a6 fix(test): make max Vitest scripts Windows-safe 2026-05-24 23:54:29 +02:00
Gio Della-Libera
9e8cc7e077 fix(doctor): migrate Feishu account bot names (#86081) 2026-05-24 14:40:16 -07:00
Vincent Koc
500c95b1ba fix(scripts): prefilter conflict marker scans 2026-05-24 23:25:05 +02:00
clawsweeper[bot]
242e8767e7 docs: add ClawSweeper review policy to AGENTS (#86197)
* docs: add ClawSweeper review policy to AGENTS

Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>

* docs: add ClawSweeper review policy to AGENTS

* docs: move ClawSweeper review policy into AGENTS.md

Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>

---------

Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
2026-05-24 16:22:16 -05:00
Andy Ye
4742db6c31 fix(installer): avoid before with npm release-age configs (#85491)
Summary:
- The PR updates the Unix installers to avoid emitting npm `--before` when raw npm config contains `min-releas ...  records a changelog fix, and widens an internal model-catalog test helper type to accept sync auth checks.
- PR surface: Source +1, Tests +421, Docs +1, Other +150. Total +573 across 7 files.
- Reproducibility: yes. The linked report at https://github.com/openclaw/openclaw/issues/84743 gives an isolat ...  exclusivity, and current main still has the source path that can generate the conflicting `--before` flag.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(installer): avoid before with npm release-age configs
- PR branch already contained follow-up commit before automerge: fix(clawsweeper): address review for automerge-openclaw-openclaw-8549…

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

Prepared head SHA: fb0762f468
Review: https://github.com/openclaw/openclaw/pull/85491#issuecomment-4522229812

Co-authored-by: Andy Ye <35905412+TurboTheTurtle@users.noreply.github.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
2026-05-24 21:18:58 +00:00
Vincent Koc
3e275a53dc fix(e2e): retry Windows kitchen sink probes 2026-05-24 23:10:33 +02:00
Vincent Koc
367d584ee3 fix(installer): install node with apk on alpine 2026-05-24 23:03:12 +02:00
Vincent Koc
acfed375ee fix(installer): detect musl linux shells 2026-05-24 22:47:05 +02:00
Dallin Romney
8ccb11cbfc perf(plugins,gateway): thread metadata snapshot + discovery through hot paths + plugin owner fixes (#84649)
* perf(plugins): thread metadata snapshot and discovery through hot paths

With the snapshot memo now actually hitting, route the snapshot's
manifestRegistry and discovery through the helper chains that already
had fast paths for them. Eliminates redundant per-call rebuilds at
two big amplifiers.

- Provider resolve paths (resolvePluginProviders /
  isPluginProvidersLoadInFlight / resolveOwningPluginIdsForProvider /
  resolveExternalAuthProfilesWithPlugins) self-service a snapshot once
  at the public entry, then thread it as a separate required arg
  through resolvePluginProviderLoadBase,
  resolveExplicitProviderOwnerPluginIds, and the setup/runtime load
  state helpers. Inner reads change from
  'params.pluginMetadataSnapshot?.x' to 'snapshot.x', no more
  enrichedParams clone. loadPluginManifestRegistryForInstalledIndex
  fires drop ~685 -> ~10 per cold start.

- Bundled-channel / auto-enable chain accepts an optional
  PluginDiscoveryResult. discoverOpenClawPlugins is fired once during
  snapshot building (resolveInstalledPluginIndexRegistry already
  produced it internally; now bubbled up through
  loadInstalledPluginIndexWithDiscovery, PluginRegistrySnapshotResult,
  and onto PluginMetadataSnapshot.discovery). load-context reads
  metadataSnapshot.discovery and passes it through
  applyPluginAutoEnable, so the bundled-channel cascade
  (collectConfiguredChannelIds, listBundledChannelIdsWith*,
  listPotentialConfiguredChannelPresenceSignals) short-circuits
  instead of each leaf re-firing discovery. Persisted-cache path is
  unchanged: no discovery on the snapshot, downstream chain handles
  its own fallback (pre-PR behavior on that path).

* test(plugins): isolate snapshot memo across tests that mock manifest registry

The snapshot memo is now process-scoped and effective (~98% hit rate).
Three test files were depending on cache misses (because the broken
cache returned them) — each test would set up its own
loadPluginManifestRegistry mock and expect a fresh derive. With the
cache fixed, an earlier test's mocked registry now leaks into later
tests in the same file.

- io.write-config.test.ts: afterEach now clears the snapshot memo so
  the 'demo' plugin mocked in the first test does not survive into
  'keeps shipped plugin install config records when index migration
  fails', which expects an empty registry to surface the 'plugin not
  found: demo' warning.

- gateway/model-pricing-cache.ts: resetGatewayModelPricingCacheForTest
  also clears the memo. Tests in model-pricing-cache.test.ts assert
  loadPluginManifestRegistryForInstalledIndex was called; the memo
  hit otherwise skips the call.

- providers.test.ts: vi.doMock loadPluginMetadataSnapshot to wrap the
  existing loadPluginManifestRegistryMock fixture. The plumbing
  commit added an auto-fetch fall-through in
  resolveOwningPluginIdsForProvider; without the mock, providers
  tests hit real disk reads and return empty registries (which is
  what surfaced as 9 unrelated-looking failures in the prior CI
  run).

* fix(plugins): preserve setup.cliBackends owner matching in provider scan

resolveOwningPluginIdsForProvider now also checks plugin.setup?.cliBackends.
The pre-PR no-registry fallback used resolvePluginContributionOwners which
includes both top-level cliBackends and setup.cliBackends; the PR's manifest
scan replacement was missing the setup case.

* fix(plugins): inherit active registry workspaceDir before loading metadata snapshot

isPluginProvidersLoadInFlight and resolvePluginProviders now resolve
env and workspaceDir once at the entry point (falling back to
getActivePluginRegistryWorkspaceDir) and pass them into both
loadPluginMetadataSnapshot and resolvePluginProviderLoadBase. Pre-fix
the snapshot used params.workspaceDir raw while the load base inherited
the active workspace, so workspace-scoped provider plugins could be
absent from the snapshot manifest registry even though owner resolution
expected them.

Regression test asserts the snapshot mock receives the active
workspaceDir when the caller omits it.

* perf(gateway): thread discovery into applyPluginAutoEnable call sites

Every gateway applyPluginAutoEnable call now passes the snapshot's
PluginDiscoveryResult so the bundled-channel cascade (collectConfiguredChannelIds
→ listBundledChannelIdsWith* → listPotentialConfiguredChannelPresenceSignals)
short-circuits instead of each leaf re-firing discovery.

Startup-time sites pull discovery from the snapshot/lookup-table they already
hold:
- server-plugin-bootstrap.ts (pluginLookUpTable)
- server-startup-plugins.ts (pluginMetadataSnapshot)
- server-startup-config.ts (pluginMetadataSnapshot)
- server-plugins.ts (pluginLookUpTable, both call sites)

Per-RPC sites (server.impl getRuntimeConfig callback, server-methods/channels
status + start handlers, server-methods/send) source discovery via
getCurrentPluginMetadataSnapshot using the runtime config to validate
compatibility. Falls through to the original slow path when the snapshot is
absent or incompatible.
2026-05-24 13:44:03 -07:00
Vincent Koc
8bf4f7d4a8 fix(ui): split control ui runtime chunks 2026-05-24 22:20:33 +02:00
tanshanshan
fe34141a3d refactor(config): extract GoogleChat schema into zod-schema.providers-googlechat.ts (#82100)
Merged via squash.

Prepared head SHA: 7555272656
Co-authored-by: tanshanshan <22539261+tanshanshan@users.noreply.github.com>
Co-authored-by: altaywtf <9790196+altaywtf@users.noreply.github.com>
Reviewed-by: @altaywtf
2026-05-24 23:03:35 +03:00
Vincent Koc
6cc8244333 fix(update): suppress internal handoff version warnings 2026-05-24 21:42:42 +02:00
Vincent Koc
0acc3e3216 test(e2e): select installable bundled plugins 2026-05-24 21:36:08 +02:00
Vincent Koc
43252c8099 fix(scripts): harden Windows native opus install 2026-05-24 21:28:09 +02:00
Brian Potter
efd88dc00d fix(agents): match runtime policy entries when session provider is empty (#85970)
Merged via squash.

Prepared head SHA: 1f081b3a8c
Co-authored-by: potterdigital <197414865+potterdigital@users.noreply.github.com>
Co-authored-by: altaywtf <9790196+altaywtf@users.noreply.github.com>
Reviewed-by: @altaywtf
2026-05-24 22:01:21 +03:00
Vincent Koc
0a98559440 fix(scripts): harden Windows generated formatting 2026-05-24 20:50:07 +02:00
David
07f500aa56 fix(mcp): bound tools/list during catalog discovery (#85063)
Summary:
- The branch adds a 1500 ms internal timeout to bundled MCP `tools/list` catalog discovery, adds slow and hung stdio MCP regression tests, and records the fix in `CHANGELOG.md`.
- PR surface: Source +2, Tests +216, Docs +1. Total +219 across 3 files.
- Reproducibility: yes. The current-main source path is high confidence: bundled MCP connects successfully, then calls `client.listTools` without request options, and the upstream SDK defaults that request to 60000 ms.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(mcp): use internal tools list timeout
- PR branch already contained follow-up commit before automerge: fix(mcp): bound tools/list during catalog discovery
- PR branch already contained follow-up commit before automerge: fix(clawsweeper): address review for automerge-openclaw-openclaw-8506…

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

Prepared head SHA: bbbfb9f059
Review: https://github.com/openclaw/openclaw/pull/85063#issuecomment-4511554739

Co-authored-by: nxmxbbd <32288+nxmxbbd@users.noreply.github.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
2026-05-24 18:37:28 +00:00
Vincent Koc
dfa1a51225 fix(test): focus plugin binding Docker smoke 2026-05-24 20:28:43 +02:00
Vincent Koc
a4fab83b55 test(e2e): fail release memory indexing errors 2026-05-24 20:12:43 +02:00
Vincent Koc
af07769871 test(daemon): fail launchd integration bootstrap errors 2026-05-24 19:57:59 +02:00
Omar Shahine
5c7980fa11 feat(imessage): support thumb approval reactions (#85952)
* feat(imessage): support thumb approval reactions

Mirrors openclaw#85477 (WhatsApp) for the iMessage channel. iMessage can now
deliver exec/plugin approval prompts via the existing imsg/BlueBubbles
transport and resolve approvals from 👍 (allow-once) / 👎 (deny) tapbacks.
Allow-always remains on the manual /approve <id> allow-always fallback.

What changed:
- New approval surfaces under extensions/imessage/src/:
  approval-auth.ts, approval-resolver.ts, approval-reactions.ts,
  approval-handler.runtime.ts, approval-native.ts (+ tests for each).
- channel.ts wires base.approvalCapability to the new iMessage capability.
- send.ts appends the 👍/👎 hint to outbound /approve prompts and registers
  the reaction binding (keyed by accountId + chat_guid/chat_identifier/
  chat_id/handle + messageId) after a successful send.
- monitor/monitor-provider.ts resolves approval reactions ahead of the
  normal inbound decision pipeline so resolution bypasses
  reactionNotifications gating and runs its own actor authorization.
- runtime.ts now exports getIMessageRuntime / getOptionalIMessageRuntime so
  approval-reactions can open a persistent keyed store for binding state
  across gateway restarts.

What did NOT change:
- Core approval surfaces in src/gateway/server-methods/* and src/infra/*
  remain channel-agnostic; the channels.imessage.allowFrom field already
  exists and is reused as the approver list for reactions.
- Other channels and the manual /approve sender-authorized path are
  untouched.

* fix(imessage): address codex review findings on thumb approvals

Addresses 15 findings from the multi-angle codex review:

Critical (correctness / blocking):
- Register CHANNEL_APPROVAL_NATIVE_RUNTIME_CONTEXT_CAPABILITY in the iMessage
  monitor so the gateway can actually deliver native approval prompts via
  approval-handler.runtime.ts (it was dead code without the context lease).
- DM tapback approvals never resolved because send keyed by handle while
  inbound preferred chat_guid. Register and look up under EVERY available
  conversation key (chat_guid / chat_identifier / chat_id / handle); inbound
  probes them all and accepts the first hit.
- Reaction binding now requires the bridge's GUID string (rejecting numeric
  ROWIDs) so the binding key matches inbound reacted_to_guid.
- Outbound regex now requires both a canonical `ID: <approvalId>` header AND
  a matching `/approve <id> <decision>` line, so non-approval messages that
  legitimately mention /approve syntax no longer get a phantom reaction
  binding (and can no longer resolve a colliding live approval).
- Drop is_from_me reaction events so cross-device echoes of the operator's
  own tap cannot self-approve when their handle is in allowFrom.

High (operability / cleanup):
- Non-ApprovalNotFound errors now log at warn via the runtime child logger
  (no longer hidden behind OPENCLAW_LOG_LEVEL=debug).
- In-memory binding is cleared on successful resolve so a toggle 👍👎 (or
  chat.db replay) does not refire and emit a misleading 'expired approval'
  log line. Removed tapbacks are also owned by the shortcut and not surfaced
  as noisy reaction system events.
- Move resolveIMessageReactionContext (and its helpers) to a slim
  monitor/reaction-context.ts so approval-reactions.ts no longer transitively
  pulls monitor/inbound-processing.ts (14+ heavy runtime modules) into the
  hot channel.ts entrypoint per extensions/CLAUDE.md.

Medium (consistency / future-proofing):
- Native runtime exec pending payload now passes agentId, ask, and
  sessionKey through buildExecApprovalPendingReplyPayload so the two
  delivery routes produce identical operator-visible prompts.
- Both delivery paths now use addIMessageApprovalReactionHintToText (single
  insertion point after ID:) so the hint cannot be double-emitted by the
  native runtime path bypassing the idempotency guard.
- Extract replaceApprovalIdPlaceholder into a shared approval-text.ts that
  escapes `$` in the replacement string so an approvalId containing
  `$&`/`$1`-`$9`/`$$` cannot interpolate into the outbound text.
- In-memory Map now stores TTL alongside each entry and prunes expired
  bindings on each register so the gateway no longer accumulates an
  unbounded reaction-target Map.
- bindPending refuses to bind when accountId is missing or the approval is
  already expired, with explicit error logs instead of silent no-ops.
- Reject chat_id=0 as a synthetic key value (chat.db ROWIDs start at 1).
- Drop dead getIMessageRuntime export — only the optional accessor is used.

Documentation:
- docs/channels/imessage.md gains an 'Approval reactions (👍 / 👎)' accordion
  documenting the reaction emoji map, allowFrom approver requirement, the
  /approve <id> allow-always manual fallback, and the deliberate change to
  /approve command authorization for users with non-empty allowFrom.
- CHANGELOG.md entry added under 2026.5.24.

Tests: 411 iMessage tests pass (was 406). Added explicit coverage for the
DM key-mismatch fix, the regex-tightening fix, the is_from_me guard, the
clear-on-success behavior, and the approval-id `$` escape.

* test(imessage): match WhatsApp approval-native test coverage

Backfills the nine cases from extensions/whatsapp/src/approval-native.test.ts
that weren't mirrored in iMessage:

- target-mode exec + plugin prompt rendering with the canonical hint
- target-mode availability when no iMessage target matches
- agentFilter / sessionFilter applied to native handling
- account-scoped target enabled/disabled per account
- shouldSuppressForwardingFallback session-origin exact-match cases
- shouldSuppressForwardingFallback off when native cannot bind (locks down
  the targets-only forwarding path the Lobster live deploy exercised)
- both-mode explicit + unscoped target suppression
- group-origin tapback approvals require explicit approvers

Tests: extensions/imessage/src/approval-native.test.ts 21 passed (was 11).
Total iMessage approval-specific cases now 49 (was 40).

* fix(imessage): preserve service-prefixed direct handles as approvers

ClawSweeper P1 review finding on #85952. normalizeIMessageApproverId was
calling looksLikeIMessageExplicitTargetId() to reject conversation-target
prefixes, but that helper also matches the imessage:/sms:/auto: service
prefixes — which are valid direct-handle forms. Any allowFrom entry like
'imessage:+15551230000' dropped to undefined, leaving approvers empty,
which:
  - silently denied reaction resolution ('reactions require explicit
    approvers'), and
  - let text /approve fall back to implicit same-chat authorization.

Fix: normalize first via normalizeIMessageHandle (strips the service
prefix), then reject only chat_id:/chat_guid:/chat_identifier:
conversation-target shapes that remain after normalization.

Tests:
  - approval-auth.test.ts: assert the resolved approver list contains the
    normalized handle, plus the corollary that a non-matching sender is
    explicitly rejected (no longer masked by the implicit-same-chat
    fallback). Add a separate case covering chat_id/chat_guid/
    chat_identifier rejection (with and without a service prefix).
  - approval-reactions.test.ts: reaction resolution end-to-end with a
    service-prefixed allowFrom entry — proves resolveIMessageApproval is
    called rather than silently denied.

Focused suite: 48 passed (was 47).

* test(imessage): satisfy strict buildPendingPayload signature in render tests

CI check:test-types caught that the render.exec/render.plugin
buildPendingPayload calls were passing accountId (not in the type
signature). The signature is { cfg, request, target, nowMs }. Replace
accountId with target on the four render-test sites so the strict
test-types pass matches the SDK contract:

  - it('renders thumbs-only reaction hints in exec approval prompts')
  - it('renders thumbs-only reaction hints in plugin approval prompts ...')
  - it('renders target-mode exec prompts with concrete thumbs-only ...')
  - it('renders target-mode plugin prompts with concrete thumbs-only ...')

Verified locally with pnpm check:test-types (tsgo:core:test +
tsgo:extensions:test). 49 approval-specific tests still pass.

* fix(imessage): probe every tapback GUID form for approval lookup

ClawSweeper P1 review finding on #85952. readApprovalReactionEvent was
only using reaction.targetGuid (the first/normalized form), but
resolveIMessageReactionContext produces reaction.targetGuids = [normalized,
raw] for both `abc-123` and `p:0/abc-123` forms. If the imsg bridge
returned 'p:0/<guid>' from send() and send.ts registered the binding under
that prefixed key, the inbound resolver probing only the unprefixed form
would miss and the tapback would silently fall through.

Fix:
- Surface every GUID candidate in IMessageApprovalReactionEvent
  (messageIdCandidates).
- maybeResolveIMessageApprovalReaction now probes each candidate in
  precedence order; first hit wins.
- On success / ApprovalNotFoundError, clear the binding under all
  candidate keys so toggle/replay does not refire.

Tests: extensions/imessage/src/approval-reactions.test.ts gains a
'resolves a reaction when the binding was registered under a p:0/…
prefixed GUID and the tapback surfaces both forms' regression case;
22/22 reaction tests pass. Full iMessage suite: 424/424.

* fix(imessage): native approval binding requires GUID, not numeric id

ClawSweeper third P1 review finding on #85952. approval-handler.runtime.ts
deliverPending was using result.messageId as the approval-reaction binding
key, but that field can be a numeric ROWID coerced to a string ('12345')
when the imsg bridge returns only message_id. Inbound tapbacks carry
reacted_to_guid which is always a GUID, so a numeric-id binding can never
match.

Fix mirrors the send.ts forwarding-path treatment:
- IMessageSendResult now exposes a separate guid?: string field, populated
  from the same resolveOutboundMessageGuid helper send.ts already uses for
  the forwarding-path binding. The generic messageId field is unchanged so
  reply-cache, echo-cache, and receipt-building paths still see the
  broadest id form.
- deliverPending now binds against result.guid; when it's undefined (numeric
  ROWID or 'ok'/'unknown' placeholders), the function returns null instead
  of binding against an id the inbound tapback can't possibly match.

Tests: approval-handler.runtime.test.ts gets a deliverPending GUID-only
binding describe block with three regression cases (numeric ROWID refused,
GUID accepted, ok/unknown placeholders refused). vi.mock isolates
sendMessageIMessage so the cases run synchronously without spawning imsg.
11 tests pass across handler.runtime + send specs.

---------

Co-authored-by: Omar Shahine <10343873+omarshahine@users.noreply.github.com>
2026-05-24 10:51:21 -07:00
Vincent Koc
ad71a998ff fix(crabbox): default macos aws runs on demand 2026-05-24 19:31:19 +02:00
Vincent Koc
e4332f7cff fix(scripts): preserve test passthrough args 2026-05-24 19:13:03 +02:00
Vincent Koc
8edc671eb4 fix(e2e): harden Windows plugin assertions 2026-05-24 19:10:10 +02:00
Vincent Koc
5f0315467b fix(test): mount upgrade survivor helper 2026-05-24 18:59:34 +02:00
Ayaan Zaidi
c4525104e9 style(android): sharpen voice mode surfaces 2026-05-24 22:06:36 +05:30
Ayaan Zaidi
955909c988 style(android): refine list surface spacing 2026-05-24 22:06:36 +05:30
Ayaan Zaidi
63a2f69601 fix(android): prevent stale chat during session switches 2026-05-24 22:06:36 +05:30
Ayaan Zaidi
d86ed21f3d fix(android): hide internal chat content blocks 2026-05-24 22:06:36 +05:30
Ayaan Zaidi
cc5eb972e6 feat(android): add pair new gateway action 2026-05-24 22:06:36 +05:30
Ayaan Zaidi
94bc18ad75 fix(android): keep permission setup action visible 2026-05-24 22:06:36 +05:30
Andy Ye
102555c6e0 Advance iMessage catchup cursor after live handling (#85475)
Fixes #85363.

Thanks @TurboTheTurtle.
2026-05-24 09:34:16 -07:00
Vincent Koc
79ee70c8ad fix(scripts): ignore forwarded arg separator 2026-05-24 18:13:14 +02:00
Vincent Koc
5a8ce6a885 fix(test): fail empty gateway startup samples 2026-05-24 17:58:12 +02:00
Vincent Koc
87a2eba427 fix(e2e): harden Windows kitchen sink assertions 2026-05-24 17:47:19 +02:00
Vincent Koc
c643370fd8 fix(e2e): harden Telegram credential paths on Windows 2026-05-24 17:17:28 +02:00
Ayaan Zaidi
be9bb775a5 fix(android): complete qr setup operator handoff 2026-05-24 20:38:57 +05:30
Ayaan Zaidi
0b55a6363e fix(android): align setup pairing scopes 2026-05-24 20:38:57 +05:30
Vincent Koc
dbc08f64c1 fix(test): copy cleanup smoke prepare hook 2026-05-24 16:57:26 +02:00
clawsweeper[bot]
675158c896 fix(secrets): allow hash in exec SecretRef ids (#86072)
Summary:
- The branch widens exec SecretRef id validation/schema/docs/test vectors to allow `#` selector syntax, adds a changelog entry, and includes a small `npm pack` filename helper cleanup.
- Reproducibility: yes. Source inspection on current main shows the shared exec SecretRef validator omits `#`, matching the linked gateway startup failure before resolver execution.

Automerge notes:
- PR branch already contained follow-up commit before automerge: docs(secrets): document hash exec SecretRef ids
- PR branch already contained follow-up commit before automerge: docs(secrets): sync exec SecretRef hash pattern
- PR branch already contained follow-up commit before automerge: fix(secrets): allow hash in exec SecretRef ids
- PR branch already contained follow-up commit before automerge: fix(clawsweeper): address review for automerge-openclaw-openclaw-8073…

Validation:
- ClawSweeper review passed for head 1cf53d95f4.
- Required merge gates passed before the squash merge.

Prepared head SHA: 1cf53d95f4
Review: https://github.com/openclaw/openclaw/pull/86072#issuecomment-4528994482

Co-authored-by: Andy Ye <andy@Andys-MacBook-Pro-2.local>
Co-authored-by: Andy Ye <andylye@outlook.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
2026-05-24 14:56:17 +00:00
Vincent Koc
694d45e535 fix(media): use static image compression metadata 2026-05-24 16:47:59 +02:00
Peter Steinberger
01c5ab8d13 fix(release): verify large plugin npm packs 2026-05-24 15:33:17 +01:00
Vincent Koc
7e51f83aec fix(test): require kitchen sink diagnostic canaries 2026-05-24 16:27:46 +02:00
Vincent Koc
483d7be6c4 fix(scripts): harden Windows upgrade survivor recipe 2026-05-24 16:05:24 +02:00
Vincent Koc
102b2c18e9 fix(installer): count verify progress stage 2026-05-24 15:50:28 +02:00
Tak Hoffman
5bffd17e01 fix: Refine PR template for review state (#86054) 2026-05-24 08:49:02 -05:00
Vincent Koc
125d82cab2 fix(test): repair split agent shard runs 2026-05-24 15:37:59 +02:00
Vincent Koc
ce48e4c197 fix(codex): harden Windows protocol formatting 2026-05-24 15:37:13 +02:00
clawsweeper[bot]
dd01a2e789 fix(openrouter): use endpoint context limits (#86041)
Summary:
- The branch updates OpenRouter dynamic model capability parsing to prefer `top_provider.context_length`, bump ... sk cache version, adds regression coverage and a changelog entry, and adds script helper declaration files.
- Reproducibility: yes. from source and live catalog evidence rather than an authenticated inference turn. Cur ... catalog currently reports a smaller endpoint-specific `top_provider.context_length` for the reported model.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(openrouter): use endpoint context limits
- PR branch already contained follow-up commit before automerge: fix(clawsweeper): address review for automerge-openclaw-openclaw-8594…

Validation:
- ClawSweeper review passed for head 76fcc362d2.
- Required merge gates passed before the squash merge.

Prepared head SHA: 76fcc362d2
Review: https://github.com/openclaw/openclaw/pull/86041#issuecomment-4528646655

Co-authored-by: Andy Ye <35905412+TurboTheTurtle@users.noreply.github.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
2026-05-24 13:32:44 +00:00
Peter Steinberger
8473e8933a test(qa): remove brittle capability flip setup turn 2026-05-24 14:30:59 +01:00
Ayaan Zaidi
5cfb12fa5d fix(telegram): migrate account topic cache sidecars 2026-05-24 18:58:02 +05:30
Ayaan Zaidi
eb9b882dae fix(telegram): migrate legacy cache sidecars 2026-05-24 18:58:02 +05:30
Vincent Koc
5be62e779b fix(scripts): harden Windows ZAI fallback repro 2026-05-24 15:11:37 +02:00
Ayaan Zaidi
400d90a4da style(android): sharpen v2 screen rhythm 2026-05-24 18:37:31 +05:30
Peter Steinberger
c91c3c6e5a test(qa): extend capability flip setup budget 2026-05-24 14:02:22 +01:00
Ayaan Zaidi
24ddd18ae1 fix(android): simplify gateway status copy 2026-05-24 18:24:51 +05:30
Ayaan Zaidi
cec52bd279 fix(android): route offline voice to gateway setup 2026-05-24 18:22:28 +05:30
Vincent Koc
581c8a6375 fix(scripts): harden Windows control UI i18n commands 2026-05-24 14:47:07 +02:00
Ayaan Zaidi
5c15859759 fix(android): stop operator chat subscription 2026-05-24 18:16:01 +05:30
Vincent Koc
a72b11d29a fix(test): fail missing kitchen sink rss samples 2026-05-24 14:44:14 +02:00
Peter Steinberger
c7d4e9e1c2 test(qa): widen capability flip restart budget 2026-05-24 13:38:54 +01:00
Ayaan Zaidi
60e6ccdb8c fix(android): smooth gateway pairing recovery 2026-05-24 18:05:40 +05:30
Vincent Koc
6d9b3887ea fix(test): suppress rolldown timing noise 2026-05-24 14:26:19 +02:00
Ayaan Zaidi
01b284cac0 style(android): fix talk mode ktlint formatting 2026-05-24 17:51:53 +05:30
Ayaan Zaidi
996d07ee46 fix(telegram): store topic cache in plugin state 2026-05-24 17:38:27 +05:30
Ayaan Zaidi
2ed52969c5 fix(telegram): store bot info cache in plugin state 2026-05-24 17:38:27 +05:30
Vincent Koc
0f82c810fc fix(test): sync sparse AWS Crabbox runs from full checkout 2026-05-24 14:00:37 +02:00
Vincent Koc
71547678c7 fix(release): harden Windows cross-os command shims 2026-05-24 13:52:18 +02:00
Vincent Koc
98e09e8817 fix(test): harden Docker resource ceilings 2026-05-24 13:38:50 +02:00
Peter Steinberger
e8643f0c15 test(telegram): keep startup limiter coverage focused 2026-05-24 12:36:45 +01:00
Peter Steinberger
04d86e0f47 test(telegram): isolate startup probe limiter timing 2026-05-24 12:23:32 +01:00
Peter Steinberger
578e73f667 test(release): harden plugin prerelease checks 2026-05-24 12:02:29 +01:00
Josh Lehman
62b51a6295 fix(telegram): serialize topic dispatch replies (#85709)
* fix(telegram): serialize topic dispatch replies (clawdbot-b19)

* fix(telegram): normalize dispatch topic context

* fix(telegram): satisfy dispatch race CI checks

* fix(telegram): normalize raw code language tags

* refactor(reply): centralize turn admission

* fix(telegram): persist recovered topic routes

* fix(reply): preserve queue policy admission

* fix(reply): retain active abort owner

* fix(reply): split active abort ownership

* fix(reply): defer busy followup drains

* fix(reply): wire hook abort ownership

* fix(reply): preserve deferred queue summaries

* fix(reply): type queued summary retry

* fix(reply): abort embedded and core runs

* test(reply): keep final abort operation active

* test(reply): stabilize abort normalization test

* fix(reply): keep non-visible admission skips silent

* test(reply): avoid dispatch shard mock bleed

* fix(reply): merge deferred queue summaries

* fix(reply): abort active-lane resolver runs

* fix(reply): compose borrowed lane abort signals

* fix(reply): keep interrupt turns caller-owned

* fix(telegram): keep recovered topic history scoped

* fix(reply): retry deferred summary queues

* fix(reply): document deferred summary restore

* fix(telegram): rebuild recovered topic prompt body

* fix(reply): run admitted session ids

* fix(telegram): recover topic chat actions

* fix(reply): honor pre-dispatch aborts for handled replies

* fix(reply): guard local handled final aborts

* fix(reply): refresh admitted session files

* fix(telegram): trust final current-message marker

* fix(telegram): migrate recovered room history

* fix(telegram): scope recovered topics to current chat

* fix(reply): wait for visible reply lane ownership

* fix(telegram): pass recovered topic body to agent

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-05-24 11:49:48 +01:00
Peter Steinberger
3679151c2c test(release): stabilize plugin prerelease checks 2026-05-24 11:40:48 +01:00
Vincent Koc
295339d616 fix(test): fail live gateway false greens 2026-05-24 12:38:23 +02:00
Vincent Koc
3838e450dd fix(test): build startup artifacts for smoke scripts 2026-05-24 12:34:13 +02:00
Peter Steinberger
0a8af67c11 test(telegram): wait for startup probe slots 2026-05-24 11:21:15 +01:00
Peter Steinberger
783290f7ed test(codex): match sandbox exec-server yolo policy 2026-05-24 11:01:15 +01:00
Vincent Koc
9ff4d36c98 fix(test): fail missing explicit test targets 2026-05-24 11:46:51 +02:00
Peter Steinberger
558c1bc39a test(codex): avoid full sandbox exec-server turn run 2026-05-24 10:36:44 +01:00
Vincent Koc
bca1ac03fe fix(ci): keep Crabbox pnpm hydration shims writable 2026-05-24 11:31:36 +02:00
Vincent Koc
75ac11aca2 fix(release): harden Windows release-check npm probes 2026-05-24 11:14:41 +02:00
Peter Steinberger
cf46f2e3a0 fix(docker): parse peer-suffixed lockfile packages 2026-05-24 09:51:39 +01:00
Peter Steinberger
f799da0947 fix(docker): seed lockfile packages before prune 2026-05-24 09:33:34 +01:00
Peter Steinberger
2cd93f1c0d fix(docker): seed lockfile snapshot tarballs before prune 2026-05-24 09:18:04 +01:00
Peter Steinberger
a4ef3a2c9a test(codex): type thread start mock params 2026-05-24 08:53:29 +01:00
Peter Steinberger
11bf6424ca test(codex): avoid full sandbox run in thread-start test 2026-05-24 08:40:58 +01:00
Vincent Koc
abdd8a40cc fix(plugins): harden Windows npm package staging 2026-05-24 09:32:17 +02:00
Peter Steinberger
c14a0c6d63 test(codex): complete sandbox turn inline 2026-05-24 08:19:01 +01:00
Vincent Koc
a56f452972 fix(release): harden Windows npm shim verification 2026-05-24 09:02:44 +02:00
Peter Steinberger
f8789599f0 test(release): type metadata snapshot mock params 2026-05-24 07:56:34 +01:00
Peter Steinberger
e9ca3115f0 test(release): finish plugin metadata prerelease sync 2026-05-24 07:42:56 +01:00
Rubén Cuevas
501f2cbfe4 fix(update): avoid broad tag fetches for dev updates (#84737)
Summary:
- The PR changes dev-channel git updates to fetch branches with `--no-tags`, adds targeted fetching for explicit dev tag refs, updates update-runner tests, and adds a changelog entry.
- Reproducibility: yes. Current main source shows dev updates still run a broad tag fetch, and the PR body sup ... al local bare-remote moved-tag reproducer showing that command fails before the branch update can continue.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(update): avoid broad tag fetches for dev updates

Validation:
- ClawSweeper review passed for head 733680b1bc.
- Required merge gates passed before the squash merge.

Prepared head SHA: 733680b1bc
Review: https://github.com/openclaw/openclaw/pull/84737#issuecomment-4503692161

Co-authored-by: Ruben Cuevas <hi@rubencu.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
2026-05-24 06:37:21 +00:00
homer-byte
4d150209c3 Fix iMessage slash command acknowledgements (#82642)
Merged via squash.

Prepared head SHA: ecc8791393
Co-authored-by: homer-byte <262247270+homer-byte@users.noreply.github.com>
Co-authored-by: omarshahine <10343873+omarshahine@users.noreply.github.com>
Reviewed-by: @omarshahine
2026-05-23 23:33:33 -07:00
Peter Steinberger
02f53e6453 test(release): align prerelease contracts 2026-05-24 07:23:32 +01:00
Peter Steinberger
56eb23dda4 test(release): align plugin prerelease checks 2026-05-24 06:47:42 +01:00
Peter Steinberger
0ba6b23534 fix(docker): copy prepare hook before install 2026-05-24 06:21:29 +01:00
Peter Steinberger
d6c9387c0f fix: share signed thinking replay policy 2026-05-24 06:10:47 +01:00
Peter Steinberger
906476af0c fix: preserve signed thinking tool ids 2026-05-24 06:10:47 +01:00
NianJiuZst
41329c0e14 fix(memory): strip invalid thinking signatures for signed-thinking providers 2026-05-24 06:10:47 +01:00
Vincent Koc
d21abb88e4 fix(scripts): harden Windows install checks 2026-05-24 07:03:49 +02:00
Peter Steinberger
b972ac1940 fix(release): keep private QA markers out of bundled alias code 2026-05-24 06:03:21 +01:00
Peter Steinberger
fdfcb0795a fix(discord): harden realtime voice wake joins 2026-05-24 05:54:10 +01:00
Peter Steinberger
3839b48615 test(parallels): harden release VM smoke isolation 2026-05-24 05:50:03 +01:00
Peter Steinberger
0f83c93740 fix: keep blank agent allowlists fail closed (#85849) 2026-05-24 05:40:16 +01:00
Peter Steinberger
88aa713c03 fix: harden session allowlist glob matching (#85849) (thanks @SebTardif) 2026-05-24 05:40:16 +01:00
Sebastien Tardif
1463d3d72c fix(security): replace regex wildcard matching with linear-time glob in session-visibility
The agentToAgent allow-pattern matcher converted user wildcards like
`*a*b*c*` into `^.*a.*b.*c.*$` via RegExp.  Multiple overlapping
`.*` groups cause O(n^k) polynomial backtracking against non-matching
input, where k is the number of wildcards.

Replace the regex path with a segment-based glob matcher that splits on
`*` and checks prefix/suffix/interior segments in order.  The new
matcher runs in O(n*k) worst case and eliminates the regex engine
entirely from this path.

Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
2026-05-24 05:40:16 +01:00
Peter Steinberger
ae9308bfe0 docs(changelog): note restart recovery notice 2026-05-24 05:38:59 +01:00
Peter Steinberger
32631eb9d4 fix(telegram): normalize legacy action targets 2026-05-24 05:38:59 +01:00
Paul Frederiksen
cf61b876ec fix: notify chat when main session recovery fails 2026-05-24 05:38:59 +01:00
NianJiu
d4e42d61c9 fix(minimax): normalize OAuth token expiry to absolute millisecond timestamp (#83480)
* fix(minimax): normalize OAuth token expiry to absolute millisecond timestamp

MiniMax returns expired_in from the token endpoint as a relative duration
in seconds (standard OAuth expires_in semantics), but the auth profile
store's hasUsableOAuthCredential() expects an absolute millisecond
timestamp.  Without conversion the token appears perpetually expired,
triggering a slow OAuth refresh network call to api.minimaxi.com on
every request — the root cause of the 30-50s auth-stage delay.

Fixes #83449.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(minimax): cover oauth expiry normalization

* fix: polish minimax oauth expiry normalization (#83480) (thanks @NianJiuZst)

* fix: update minimax raw fetch allowlist (#83480)

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-05-24 05:21:22 +01:00
David
55f994a8d0 fix(memory-wiki): show vault totals in palace summary (#85824)
* fix(memory-wiki): show vault totals in palace summary

* fix(memory-wiki): avoid zero-page legacy question label

---------

Co-authored-by: nxmxbbd <32288+nxmxbbd@users.noreply.github.com>
2026-05-24 05:11:12 +01:00
clawsweeper[bot]
8deb1ef7db Isolate boot-md startup sessions (#85919)
Summary:
- The branch updates gateway boot startup handling to use an `agent:<id>:boot` session, suppress prompt persis ...  that boot mapping after the run, and adds focused gateway boot regression coverage plus a changelog entry.
- Reproducibility: yes. there is a high-confidence source reproduction path: current main passes the generated ... idence of repeated persisted boot prompts. I did not execute the gateway scenario in this read-only review.

Automerge notes:
- PR branch already contained follow-up commit before automerge: Fix boot-md test lint
- PR branch already contained follow-up commit before automerge: Isolate boot-md startup sessions

Validation:
- ClawSweeper review passed for head 5d5338c2d9.
- Required merge gates passed before the squash merge.

Prepared head SHA: 5d5338c2d9
Review: https://github.com/openclaw/openclaw/pull/85919#issuecomment-4527318708

Co-authored-by: Andy Ye <35905412+TurboTheTurtle@users.noreply.github.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
2026-05-24 04:05:45 +00:00
AirLin
d0751111a4 Guard OpenAI image compression for PNG outputs (#85776)
* Guard OpenAI image compression for PNG outputs

* Fix OpenAI image compression type narrowing

* docs(changelog): note OpenAI PNG compression fix

* Revert "docs(changelog): note OpenAI PNG compression fix"

This reverts commit b11e4bff01.

---------

Co-authored-by: airlin <airlin@airlins-Mac-mini.local>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-05-24 05:01:55 +01:00
rendrag-git
1d1a7c26d8 fix(agents): clamp proxy completions caps after payload shaping (#85889)
Clamp proxy-like OpenAI Chat Completions output caps against the estimated final outbound request payload after compatibility transforms. This prevents strict local/API-compatible servers from rejecting requests whose prompt already consumes part of the effective context window, while avoiding over-clamping dropped replay turns.

Co-authored-by: rendrag-git <253747599+rendrag-git@users.noreply.github.com>
2026-05-24 04:57:23 +01:00
Peter Steinberger
17dcdead00 fix: gate discord realtime voice by wake name (#85915) 2026-05-24 04:47:16 +01:00
Gio Della-Libera
c074d09f1e fix(update): ignore inherited launchd xpc for respawn (#85789) 2026-05-23 20:42:05 -07:00
Gio Della-Libera
6b337ff3ea fix: allow symlinked workspace write parents (#85818) 2026-05-23 20:42:01 -07:00
Gio Della-Libera
af765100ff fix(agents): preserve latest thinking replay signatures (#85579) 2026-05-23 20:41:57 -07:00
Gio Della-Libera
e6d5b7db96 fix(plugins): return plugin gateway method results (#85785) 2026-05-23 20:41:52 -07:00
Gio Della-Libera
068b9acb51 fix(gateway): hide duplicate ACP chat replies (#85775) 2026-05-23 20:41:45 -07:00
Gio Della-Libera
566d8cdf39 fix(update): ignore restart script spawn failures (#85761) 2026-05-23 20:41:40 -07:00
Gio Della-Libera
617335250e fix(telegram): honor explicit default account warning (#85752) 2026-05-23 20:41:35 -07:00
Gio Della-Libera
82af6119fa fix: honor OPENCLAW_HOME defaults (#85802)
* fix: honor OPENCLAW_HOME defaults

* fix(install): preserve openclaw home upgrade defaults

* fix(install): satisfy shellcheck tilde patterns
2026-05-23 20:39:59 -07:00
Gio Della-Libera
2e8dee7f28 fix(browser): avoid cold mac chrome version timeouts (#85460) 2026-05-23 20:39:47 -07:00
Gio Della-Libera
76221b53c2 fix(doctor): prune stale bundled plugin paths (#85038) 2026-05-23 20:39:42 -07:00
Peter Steinberger
c38a9a883a fix: label meeting note transcript speakers
Include speaker-labeled transcript lines in Meeting Notes summaries and structured summary artifacts.
2026-05-24 04:29:01 +01:00
Peter Steinberger
8f783cdcad fix(release): keep memory plugin npm package small 2026-05-24 04:27:42 +01:00
Peter Steinberger
bae0e3fae5 fix(release): speed plugin bundled dependency installs 2026-05-24 04:27:42 +01:00
Peter Steinberger
4daf1aab55 fix(release): keep plugin bundled install lock-compatible 2026-05-24 04:27:42 +01:00
Peter Steinberger
7a85f1ee94 test(matrix): stabilize thread binding sweep persistence 2026-05-24 04:27:42 +01:00
Kaspre
6008375655 fix(gateway): honor restart drain budget for embedded runs
Honor configured restart drain budgets for embedded runs and avoid a second active-work drain after forced deferral timeout restarts.

Includes maintainer changelog entry.
2026-05-24 04:22:27 +01:00
Peter Steinberger
6e994ad343 fix: preserve provider defaults during config saves (#85903) 2026-05-24 04:22:15 +01:00
Peter Steinberger
7439d78297 fix(release): accept sha-verified publish evidence 2026-05-24 04:17:40 +01:00
Enjou
3b3b2cca9c fix(ui): handle empty strings with minLength constraint in config save (#85850)
* fix(ui): handle empty strings with minLength constraint in config save

Fixes #85831

When saving config in Control UI, required string fields with minLength
constraint (e.g., z.string().min(1)) were sent as empty strings instead
of being unset. This prevented schema defaults from applying.

Solution: coerce empty strings with minLength > 0 to undefined, allowing
schema defaults to take effect during validation.

Added 5 unit tests covering edge cases.

* fix(types): add minLength and maxLength to JsonSchema type
2026-05-24 04:15:21 +01:00
Peter Steinberger
cbdc24895e docs: add changelog for aborted subagent fix 2026-05-24 04:08:15 +01:00
Peter Steinberger
fc4bd448b6 fix: prefer aborted stop reason over blocked lifecycle 2026-05-24 04:08:15 +01:00
Peter Steinberger
8df01a8683 fix: treat aborted subagent lifecycle events as killed 2026-05-24 04:08:15 +01:00
JARVIS-Glasses
4d502b3d1e fix(agents): treat aborted subagent runs as terminal 2026-05-24 04:08:15 +01:00
Peter Steinberger
ba94ca5eff docs: update changelog for whatsapp reply fence 2026-05-24 04:02:59 +01:00
Cavit Erginsoy
bd91107fc6 Fix foreground reply fence visibility 2026-05-24 04:02:59 +01:00
Peter Steinberger
841cb121fb fix(twitch): cancel auth retry disconnects 2026-05-24 03:55:49 +01:00
Peter Steinberger
08159d87d2 fix: address PR review comments 2026-05-24 03:55:49 +01:00
Peter Steinberger
8cc93293a1 fix(tools): tolerate out-of-scope autoreview findings 2026-05-24 03:55:49 +01:00
Peter Steinberger
6a482584ee fix(ci): address review sweep regressions 2026-05-24 03:55:49 +01:00
Peter Steinberger
679b6776d5 fix(node): avoid stale TLS pins when retargeting 2026-05-24 03:55:49 +01:00
Peter Steinberger
97c63e63b1 fix(cli): keep secrets configure JSON singular 2026-05-24 03:55:49 +01:00
Peter Steinberger
9177860373 fix(twitch): wait through auth retry disconnects 2026-05-24 03:55:49 +01:00
Peter Steinberger
6ce9e0dd9b fix(cli): keep completion and Twitch races bounded 2026-05-24 03:55:49 +01:00
Peter Steinberger
e9bf1113fa fix(twitch): cancel pending clients during shutdown 2026-05-24 03:55:49 +01:00
Peter Steinberger
5b2703e24d fix(plugins): avoid Signal and Twitch setup regressions 2026-05-24 03:55:49 +01:00
Peter Steinberger
c617009cbf fix(plugins): stabilize Twitch and Signal setup 2026-05-24 03:55:49 +01:00
Peter Steinberger
25ccadd22a fix(acp): require allow option for auto approvals 2026-05-24 03:55:49 +01:00
Peter Steinberger
bee15d4fa2 fix(browser): validate inputs and redact remote URLs 2026-05-24 03:55:49 +01:00
Peter Steinberger
9410eb30cf fix(cli): preserve explicit command intent 2026-05-24 03:55:49 +01:00
Peter Steinberger
a4e95cf7b1 fix(cli): bound node media file writes 2026-05-24 03:55:49 +01:00
Peter Steinberger
181d55ee1b docs(changelog): note CLI and plugin bug fixes 2026-05-24 03:55:49 +01:00
Peter Steinberger
6d6b2479ad fix(gateway): scope imported history identity 2026-05-24 03:55:49 +01:00
Peter Steinberger
eeb5f12293 fix(plugins): fail stalled runtime operations 2026-05-24 03:55:49 +01:00
Peter Steinberger
9ab0af270a fix(cli): keep plugin command metadata intact 2026-05-24 03:55:49 +01:00
Peter Steinberger
15ff89bf5d fix(cli): preserve command option state 2026-05-24 03:55:49 +01:00
Peter Steinberger
308af85991 fix(cli): harden generated completions 2026-05-24 03:55:49 +01:00
Peter Steinberger
459cee5315 fix(cli): reject malformed timeout options 2026-05-24 03:55:49 +01:00
Kaspre
96959ec3d7 fix(codex): defer native-hook-relay unregister to avoid cleanup race
Keep successful Codex native hook relays alive through a bounded grace window so late hook callbacks still reach OpenClaw enforcement, while interrupted, aborted, timed-out, and failed turns unregister immediately.\n\nCo-authored-by: Kaspre <kaspre@gmail.com>
2026-05-24 03:53:00 +01:00
NianJiu
0abedd546a fix(models): preserve source snapshots for SecretRef providers
* fix(models): preserve source snapshots for SecretRef providers

* docs: add models SecretRef changelog entry

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-05-24 03:48:05 +01:00
Peter Steinberger
bc6d430d00 fix: recover discord realtime playback state 2026-05-24 03:44:31 +01:00
samzong
31145e0dd9 [Fix] Preflight runtime SecretRefs before config writes (#84454)
* fix(config): preflight runtime secret refs before writes

* fix(config): restore include rollback env

* docs(changelog): note SecretRef config preflight

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-05-24 03:40:15 +01:00
Shakker
81dee15406 test: narrow transcript rewrite message content access 2026-05-24 03:37:43 +01:00
Shakker
5534cad6fc test: cover control ui source reply final retention 2026-05-24 03:37:43 +01:00
Shakker
5e2857477b fix: keep source reply finals live in control ui 2026-05-24 03:37:43 +01:00
Shakker
321d98b982 test: cover source reply media transcript backing 2026-05-24 03:37:43 +01:00
Shakker
39226ea35b fix: back source reply media in transcripts 2026-05-24 03:37:43 +01:00
Shakker
b074dc5395 fix: guard transcript source reply rewrites 2026-05-24 03:37:43 +01:00
Shakker
17fc1d1143 fix: ignore replayed empty TUI finals 2026-05-24 03:37:43 +01:00
Shakker
a5568ddfe0 fix: preserve source reply metadata through TTS 2026-05-24 03:37:43 +01:00
Shakker
a10e152519 fix: avoid duplicate media source reply transcripts 2026-05-24 03:37:43 +01:00
Shakker
a238f03521 fix: preserve reply metadata through media normalization 2026-05-24 03:37:43 +01:00
Shakker
6a0e030a47 fix: avoid double terminal chat events for source replies 2026-05-24 03:37:43 +01:00
Shakker
f5b415f138 fix: bound Codex post-reasoning source reply waits 2026-05-24 03:37:43 +01:00
Shakker
c93dda9423 fix: keep long Codex source replies alive 2026-05-24 03:37:43 +01:00
Shakker
84d278ad81 fix: keep TUI watchdog runs active 2026-05-24 03:37:43 +01:00
Shakker
59b8aea09e fix: render late source reply finals in TUI 2026-05-24 03:37:43 +01:00
Shakker
589fd923ce docs: add TUI source reply changelog 2026-05-24 03:37:43 +01:00
Shakker
84ac31b6db fix: broadcast source reply finals for chat runs 2026-05-24 03:37:43 +01:00
Shakker
bfcd8017c4 fix: preserve reply payload metadata 2026-05-24 03:37:43 +01:00
WhatsSkiLL
b13166bc0c fix: gracefully escalate process supervisor cancellations (#85865)
* fix: gracefully escalate supervisor cancellations

* fix: preserve process-tree cancellation during grace

* fix: satisfy signal monitor allSettled lint

* fix(process): split graceful cancel signal escalation

---------

Co-authored-by: JARVIS-Glasses <284122573+JARVIS-Glasses@users.noreply.github.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-05-24 03:35:37 +01:00
brokemac79
f55e98671a fix: preserve internal handoff status attribution [AI-assisted] (#85726)
* fix: preserve status attribution for internal handoffs

* fix: preserve internal handoff status attribution (#85726) (thanks @brokemac79)

* fix: surface internal fallback failures (#85726)

* fix: preserve internal handoff session continuity (#85726)

* fix: skip internal fallback auto overrides (#85726)

* fix: preserve direct internal handoff state (#85726)

* fix: authorize internal announce handoff (#85726)

* fix: preserve handoff accounting without hiding transcript (#85726)

* test: fix session-store cli backend fixture (#85726)

* fix: trust-gate handoff accounting preservation (#85726)

* fix: avoid stale preserve-mode session writes (#85726)

* fix: avoid preserve-mode session identity writes (#85726)

* fix: hide internal handoff usage footers (#85726)

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-05-24 03:24:27 +01:00
Peter Steinberger
029472c6de fix: keep discord realtime audio playback alive 2026-05-24 03:20:01 +01:00
Masato Hoshino
069c7b87eb fix(browser): thread snapshot timeoutMs through agent tool and helpers (#75702)
Summary:
- Threads browser snapshot `timeoutMs` through the agent action, client/proxy request, snapshot route plan, Ch ...  Playwright/CDP helpers, regression tests, changelog, and one JSDoc-only shrinkwrap script type annotation.
- Reproducibility: yes. source reproduction is high-confidence: current main accepts top-level browser `timeou ...  helpers drop it. I did not rerun the original macOS or Browserbase live scenario in this read-only review.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(browser): apply default snapshot timeout to proxy path and add Pl…
- PR branch already contained follow-up commit before automerge: docs(changelog): add browser snapshot timeout propagation fix entry
- PR branch already contained follow-up commit before automerge: fix(browser): thread snapshot timeoutMs through agent tool and helpers
- PR branch already contained follow-up commit before automerge: fix(clawsweeper): address review for automerge-openclaw-openclaw-7570…

Validation:
- ClawSweeper review passed for head 0eec196962.
- Required merge gates passed before the squash merge.

Prepared head SHA: 0eec196962
Review: https://github.com/openclaw/openclaw/pull/75702#issuecomment-4359923127

Co-authored-by: masatohoshino <g515hoshino@gmail.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
2026-05-24 02:15:58 +00:00
Rohit
d581415026 Disable Chrome MCP telemetry watchdog by default (#85886)
Summary:
- The PR adds the Chrome DevTools MCP `--no-usage-statistics` default launch arg, honors explicit profile usage-statistics `mcpArgs`, adds regression tests, and adds a changelog entry.
- Reproducibility: yes. source-reproducible: current main builds Chrome MCP launch args without the upstream o ... etry is initialized. I did not run a fresh failing current-main process leak loop in this read-only review.

Automerge notes:
- PR branch already contained follow-up commit before automerge: Disable Chrome MCP telemetry watchdog by default

Validation:
- ClawSweeper review passed for head 68249b1f58.
- Required merge gates passed before the squash merge.

Prepared head SHA: 68249b1f58
Review: https://github.com/openclaw/openclaw/pull/85886#issuecomment-4526997996

Co-authored-by: Rohit <rohitjavvadi2@gmail.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
2026-05-24 02:02:37 +00:00
Peter Steinberger
12f82270cf perf: cache stable gateway metadata 2026-05-24 02:54:28 +01:00
openclaw-release-bot
fc3c9791ad chore(release): update appcast for 2026.5.22 2026-05-24 01:52:49 +00:00
Dmitry Golubev
7b3be04582 fix(codex): ensure codex subagent bootstrap parity with pi subagents - only inject AGENTS.md and TOOLS.md (#85811)
* fix: limit Codex native subagent bootstrap context

* fix: preserve Codex turn instructions accounting

* fix: split Codex workspace instruction renderers

* fix(codex): keep persona files turn-scoped

---------

Co-authored-by: Beru <beru@lastguru.lv>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-05-24 02:48:03 +01:00
Gio Della-Libera
1f28c3e42b fix(update): escape systemd update handoffs (#85414) 2026-05-23 18:44:01 -07:00
scotthuang
5dcbd385f7 fix(media-understanding): restore image description token default
Restore the describeImageWithModel default token budget to the helper-level 4096-token default instead of forcing 512 before resolution.

Add regression coverage for the default and for smaller model caps, and record the user-facing fix in the changelog.

Co-authored-by: scotthuang <scotthuang@tencent.com>
2026-05-24 02:42:18 +01:00
Peter Steinberger
0cba872e38 chore: bump version to 2026.5.24 2026-05-24 02:40:16 +01:00
Peter Steinberger
6c210668ed docs: note WebChat done ordering fix 2026-05-24 02:36:07 +01:00
Neerav Makwana
c614b59f03 fix(ui): delay WebChat done indicator until reply renders
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-24 02:36:07 +01:00
Peter Steinberger
40d36b5bbc docs(talk): document realtime active-run control
Co-authored-by: Colin <colin@solvely.net>
2026-05-24 02:35:04 +01:00
Peter Steinberger
4ffa07d136 feat(discord): control active realtime voice runs
Co-authored-by: Colin <colin@solvely.net>
2026-05-24 02:35:04 +01:00
Peter Steinberger
13c0b1952e feat(ui): control active realtime talk runs
Co-authored-by: Colin <colin@solvely.net>
2026-05-24 02:35:04 +01:00
Peter Steinberger
a1f47bccb5 feat(gateway): steer realtime relay agent runs
Co-authored-by: Colin <colin@solvely.net>
2026-05-24 02:35:04 +01:00
Peter Steinberger
bbf9c45ba7 feat(talk): add realtime active-run control
Co-authored-by: Colin <colin@solvely.net>
2026-05-24 02:35:04 +01:00
Jason O'Neal
ee09481a88 fix(tui): handle German AltGr input (#83947)
Summary:
- The PR updates the TUI CustomEditor to ignore Kitty key-release events, decode German-layout AltGr printable CSI-u input, and adds regression tests plus a changelog entry.
- Reproducibility: yes. The PR body supplies a before/after PTY/raw-stdin path for the exact Kitty CSI-u bytes ... es to pi-tui, whose printable decoder rejects Alt/Ctrl AltGr input and can insert printable release events.

Automerge notes:
- PR branch already contained follow-up commit before automerge: Merge main into fix/issue-48897
- PR branch already contained follow-up commit before automerge: Merge upstream/main into fix/issue-48897
- PR branch already contained follow-up commit before automerge: chore: remove unrelated import churn from AltGr fix

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

Prepared head SHA: ccd1057c05
Review: https://github.com/openclaw/openclaw/pull/83947#issuecomment-4484076134

Co-authored-by: Jason O'Neal <jason.allen.oneal@gmail.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
2026-05-24 01:31:03 +00:00
Peter Steinberger
6e03d1ca5b docs: clarify commit fallback provenance 2026-05-24 02:27:39 +01:00
Peter Steinberger
d92501dbf3 fix(pdf): move MiniMax PDF fallback policy to metadata 2026-05-24 02:26:47 +01:00
Neerav Makwana
4f95cc3dac fix(pdf): preserve image fallback precedence 2026-05-24 02:26:47 +01:00
Neerav Makwana
89bb62e2d7 fix(pdf): use MiniMax text model fallback 2026-05-24 02:26:47 +01:00
clawsweeper[bot]
1a60c19743 fix(ui): preserve source config for Control UI saves (#85879)
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
2026-05-24 02:23:09 +01:00
Andy Tien
aa050a6e95 fix(doctor): repair stale contextWindow for DeepSeek V4 Flash (#85840)
* fix(doctor): repair stale contextWindow for DeepSeek V4 Flash

Problem:
- Older releases configured deepseek-v4-flash with contextWindow: 200000
- Official DeepSeek V4 Flash context window is 1,000,000 (1M)
- Users switching from smaller models see incorrect progress bar (e.g.,
  50% instead of 10%) because stale config value overrides catalog

Fix:
- Add 'models.providers.*.models.*.contextWindow-stale' migration
- Detects deepseek-v4-flash models with 200K contextWindow
- Repairs to 1M to match catalog default
- Handles both bare and provider-prefixed model IDs
- 7 unit tests covering repair, passthrough, edge cases

Fixes: #85834

* fix(doctor): preserve custom DeepSeek context windows

* fix(doctor): detect stale DeepSeek context windows

* fix(doctor): scope DeepSeek context repair

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-05-24 02:22:53 +01:00
scotthuang
a66898209a Feat/fix dashboard timeout error display (#85815)
* fix(gateway): broadcast error to UI when chat.send fails synchronously

* test(gateway): verify broadcastChatError is called on chat.send error

* test(gateway): import GatewayRequestContext from local server-methods barrel

Fixes the chat error-broadcast regression test so it can resolve its
type import. The previous `../types.js` path does not exist in the
gateway tree; the shared types are re-exported from
`src/gateway/server-methods/types.ts`, so the test must use `./types.js`.

Addresses ClawSweeper review on PR #85815.

---------

Co-authored-by: scotthuang <scotthuang@tencent.com>
2026-05-24 02:22:32 +01:00
AMARA
27a3290b53 fix(memory): write fallback dream diary on narrative timeout (#85821) 2026-05-24 02:22:12 +01:00
alkor2000
72744fd5fd fix(twitch): fail fast when auth provider cannot bind user (#85794)
createAuthProvider swallowed addUserForToken rejections in a .catch()
that only logged, so getClient returned and cached a ChatClient backed
by a RefreshingAuthProvider with no bound user. The failure surfaced
later as an opaque auth error on first send instead of failing fast.

Re-throw in the catch so getClient rejects and does not cache the broken
client. Adds regression tests for the rejection and the no-cache behavior.

Fixes #83853
2026-05-24 02:21:59 +01:00
Peter Steinberger
9a73ddc394 docs: clarify PR provenance roles 2026-05-24 02:16:56 +01:00
Vincent Koc
32f91503be fix(scripts): harden Windows QA runners 2026-05-24 02:55:08 +02:00
Peter Steinberger
acf265d4d5 docs(skills): prefer latest Parallels snapshots 2026-05-24 01:30:25 +01:00
狼哥
f05f243824 fix(telegram): normalize durable group retry targets (#85656)
Summary:
- The PR normalizes legacy Telegram `group:<numeric>` durable retry targets before text/media/payload/poll sends and delivered-message pinning, with regression tests and a changelog entry.
- Reproducibility: yes. Source inspection shows recovery passes `entry.to` unchanged into the Telegram outbound path, and current send resolution rejects bare `group:-100...` as a non-numeric Telegram chat ID.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(telegram): normalize durable retry pin targets
- PR branch already contained follow-up commit before automerge: fix(telegram): normalize durable group retry targets
- PR branch already contained follow-up commit before automerge: fix(clawsweeper): address review for automerge-openclaw-openclaw-8565…

Validation:
- ClawSweeper review passed for head 272bc225dd.
- Required merge gates passed before the squash merge.

Prepared head SHA: 272bc225dd
Review: https://github.com/openclaw/openclaw/pull/85656#issuecomment-4524463510

Co-authored-by: luoyanglang <hanwanlonga@gmail.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
2026-05-24 00:30:11 +00:00
clawsweeper[bot]
fa39bef389 fix #84857: skip CLI runtime harness preflight during compaction (#85862)
Summary:
- The PR skips agent-harness compaction preflight for provider-owned or configured CLI runtime sessions, adds claude-cli regression coverage, includes a changelog entry, and applies small test/type cleanups.
- Reproducibility: yes. at source level. Current main still routes provider-owned `claude-cli` runtime compaction preflight through harness selection, where `claude-cli` is not a registered embedded harness.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix #84857: skip CLI runtime harness preflight during compaction
- PR branch already contained follow-up commit before automerge: fix(clawsweeper): address review for automerge-openclaw-openclaw-8487…

Validation:
- ClawSweeper review passed for head 1dd8a88d21.
- Required merge gates passed before the squash merge.

Prepared head SHA: 1dd8a88d21
Review: https://github.com/openclaw/openclaw/pull/85862#issuecomment-4526794976

Co-authored-by: 张贵萍0668001030 <zhang.guiping@xydigit.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
2026-05-24 00:00:55 +00:00
Gio Della-Libera
4ffbd07c06 docs(policy): add policy rule reference tables (#85795) 2026-05-23 16:59:33 -07:00
Gio Della-Libera
1e2e614748 Policy: add tool posture conformance checks (#85482)
* feat(policy): add tool posture conformance

* fix(policy): attest tool alsoAllow posture
2026-05-23 16:44:42 -07:00
njuboy
a1eb765f0a fix(session-lock): enforce maxHoldMs in shouldReclaim during lock acquisition (#85764)
* fix(session-lock): enforce maxHoldMs in shouldReclaim during lock acquisition

- Adds optional maxHoldMs parameter to inspectLockPayload
- Inspect now marks locks as stale when held longer than maxHoldMs
- Passes maxHoldMs through inspectLockPayloadForSession
- acquireSessionWriteLock's shouldReclaim callback now passes maxHoldMs

This ensures that when a live process holds a lock for longer than
maxHoldMs (default 5min), other processes can reclaim it during
acquisition — matching the watchdog's existing enforcement.

Previously shouldReclaim only used staleMs (30min default), meaning
a lock held for 10+ minutes by a live PID would never be reclaimable,
causing 60s timeout failures and gateway freezes.

Closes #85762

* fix(session-lock): add dead-PID fast-path before retry loop

Adds a fast-path check at the top of acquireSessionWriteLock:
if the lock file's owner PID is dead, remove it immediately
before entering the retry loop. This saves up to timeoutMs (60s)
of futile waiting when the previous lock holder has died.

The shouldReclaim callback already handles this case, but only
iteratively through the retry loop. The fast-path eliminates
that unnecessary delay.

* fix(session-lock): enforce max hold during acquisition

* fix(session-lock): revalidate max hold safely

* fix(session-lock): honor holder max-hold policy

* fix(session-lock): keep cleanup from reclaiming live holders

* fix(session-lock): remove stale locks only when unchanged

* fix(session-lock): skip self-held max-hold reclaim

* fix(ci): refresh gateway protocol checks

---------

Co-authored-by: njuboy11 <njuboy11@users.noreply.github.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-05-24 00:38:01 +01:00
Peter Steinberger
a1c2d093c2 refactor: simplify channel catalog cache 2026-05-24 00:31:01 +01:00
Peter Steinberger
d4299dcbaa docs: codify gateway plugin metadata stability 2026-05-24 00:31:01 +01:00
Peter Steinberger
e5534dd2f3 perf: reduce gateway benchmark filesystem churn 2026-05-24 00:31:01 +01:00
Peter Steinberger
e2249d8d1e fix: order meeting notes startup around channels 2026-05-24 00:30:39 +01:00
Peter Steinberger
a0f6ce03ce fix: preserve sandbox skill overlay precedence (#85591) 2026-05-24 00:28:49 +01:00
Peter Steinberger
68487f494c fix: close remote sandbox skill symlink aliases (#85591) 2026-05-24 00:28:49 +01:00
Peter Steinberger
a8f68877a5 fix: guard remote sandbox skill roots (#85591) 2026-05-24 00:28:49 +01:00
Peter Steinberger
a3526789a4 fix: harden sandbox skill mounts (#85591) 2026-05-24 00:28:49 +01:00
Jason O'Neal
10942102e3 test(sandbox): allow remote writes under absent skill roots 2026-05-24 00:28:49 +01:00
Jason O'Neal
dd5fb1e71f fix(sandbox): filter remote skill mounts by existing roots 2026-05-24 00:28:49 +01:00
Jason O'Neal
07abb19431 test(sandbox): resolve skill roots read-only 2026-05-24 00:28:49 +01:00
Jason O'Neal
7152806950 fix(sandbox): block remote bridge writes to skills 2026-05-24 00:28:49 +01:00
Jason O'Neal
9e5b416130 fix(sandbox): block bridge writes to workspace skills 2026-05-24 00:28:49 +01:00
Jason O'Neal
1b7bf4a56f fix(sandbox): mount workspace skills read-only 2026-05-24 00:28:49 +01:00
Abdel Gomez-Perez
5c4a733912 fix(cli-runner): keep recent tail when reseed history exceeds maxHistoryChars (#83117)
* fix(cli-runner): keep recent tail when reseed history exceeds maxHistoryChars

`buildCliSessionHistoryPrompt` was prefix-slicing the rendered history,
dropping the most recent assistant turns from the reseed prompt. After
#80934 made the Claude-CLI reseed default-on, every Claude-CLI user is
exposed to this on session_expired when the rendered transcript exceeds
12288 chars. The truncation marker landed mid-word in real reproductions.

Fix:
- Tail-slice (keep the recent suffix, drop the older prefix)
- Pin the compaction summary as a prefix when present, only cap the
  post-summary transcript (loadCliSessionReseedMessages deliberately
  places the summary first)
- When the summary alone exceeds maxHistoryChars, head-slice the summary
  itself to honor the cap; drop the post-summary tail in that case
- Move the truncation marker to the lead since what follows is the
  recent tail, not what was dropped

Closes #83157

* fix(cli-runner): retain recent tail with oversize summaries

* fix(cli-runner): cap summary block plus marker against maxHistoryChars

ClawSweeper P2 on #83117 flagged that when `summaryRendered.length` is
less than `maxHistoryChars` but `summaryBlock.length` (summary + `\n\n`
separator) meets or exceeds it, the `remainingBudget <= 0` arm of
`buildCliSessionHistoryPrompt` appends the truncation marker after the
already-full summary block. A 199-char rendered summary under a 200-char
cap produced a 257-char history block — defeating the cap that prevents
reseeding fresh CLI sessions with unexpectedly huge prompts.

Fix the budget edge by truncating the summary in this branch as well so
`summary + separator + marker` stays within `maxHistoryChars`. The tail
still drops (the summary alone consumes the budget) and the marker still
leads its own line so the prompt announces what was discarded. Mirrors
the existing oversize-summary branch's pattern of head-slicing the
summary against an explicit budget that reserves marker + separator.

Add a focused regression in `session-history.test.ts` covering exactly
the gap the finding called out: `summaryRendered.length < maxHistoryChars`
with a non-empty post-summary tail. Asserts the rendered history block
stays within `maxHistoryChars` and the truncation marker is present.

* fix(cli-runner): keep tail for near-cap summaries

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-05-24 00:07:11 +01:00
Peter Steinberger
846f56642b docs: thank backup hardlink contributor (#83938) 2026-05-24 00:05:55 +01:00
Jason O'Neal
77d1157618 fix(backup): reject missing hardlink targets 2026-05-24 00:05:55 +01:00
Jason O'Neal
d8a2cd5204 fix(backup): dereference archive hardlinks 2026-05-24 00:05:55 +01:00
Peter Steinberger
d73f3ac85d refactor: split subagent delivery state 2026-05-24 00:05:48 +01:00
Peter Steinberger
3cf806d172 fix(telegram): cache outbound replies for context
Co-authored-by: Keshav's Bot <keshavbotagent@gmail.com>
2026-05-24 00:04:16 +01:00
Peter Steinberger
ec0e76792c docs: require blame-backed bug provenance 2026-05-24 00:02:22 +01:00
Jason O'Neal
cf70bdcceb fix(webchat): scope attachment button input 2026-05-23 23:59:48 +01:00
brokemac79
0c044596c5 fix(twitch): evict client manager on disconnect failure (#85796) 2026-05-23 23:58:55 +01:00
Youssef Hemimy
f0ec7309fc fix(whatsapp): serialize Error in auto-reply delivery log (#85777)
The auto-reply "delivery failed" log path passes a raw Error
under the `err` field. tslog's default JSON serialization
renders bare Error instances as `{}` because Error own data
properties are non-enumerable. Every delivery failure in
production therefore logs `err: {}`, forcing operators to
guess the underlying Baileys error from timestamp alone.

Convert Error to `{ type, message, stack }` plus own-enumerable
properties at the log site, so Boom-style subclass diagnostics
(output.statusCode, data) and custom OutboundDeliveryError
fields (stage, results) survive. Non-Error rejection values
pass through unchanged.

Tests cover Error, Error subclass (Boom-style), string
rejection, and object rejection paths.

AI-assisted: Claude Code (Opus 4.7) authored, codex review
locally addressed.
2026-05-23 23:58:51 +01:00
JC
0050245bc7 fix(gateway): omit stream-error placeholders from agent prompts (#85652)
* fix(gateway): omit stream-error placeholders from agent prompts

* fix(gateway): omit internal placeholder prompts

* fix(gateway): filter placeholder by role

* fix(gateway): preserve current prompt text

* test(plugin): align cold-boundary model normalization expectation

* fix(gateway): mark internal stream-error prompt entries

* fix(gateway): preserve empty tool prompt entries

* test(plugin): expect static xai normalization

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-05-23 23:58:47 +01:00
Peter Steinberger
bb947eed6c docs: add changelog for webchat tool result fix (#84773) 2026-05-23 23:55:10 +01:00
Jason O'Neal
83c2e96a16 fix: summarize internal webchat message tool results 2026-05-23 23:55:10 +01:00
Matthew Kern
a37ebb2d49 fix(browser): bypass managed proxy for loopback CDP
Keep browser CDP managed-proxy bypasses on the private bundled-plugin SSRF helper, strip WebSocket URL credentials before registering exact bypass URLs, and document the managed-browser loopback proxy behavior.

Co-authored-by: Matthew Kern <matthew@matthewkern.xyz>
2026-05-23 23:53:27 +01:00
Peter Steinberger
69e8039f9a fix: omit empty proxy tools (#85835) 2026-05-23 23:52:02 +01:00
rendrag-git
75081569b0 fix(agents): omit empty tools array for proxy-like openai-completions endpoints
Strict OpenAI-compatible servers (vLLM, LocalAI, llama.cpp, LM Studio) and
current OpenAI itself reject requests containing tools: []. Strip the empty
tools array (and the orphan tool_choice) from outbound chat-completions
payloads when usesExplicitProxyLikeEndpoint is true. Native OpenAI/Azure/
OpenRouter routes are byte-identical.

Supersedes #70790 at the canonical payload builder seam so the gateway,
embedded runner, and public plugin-SDK consumers (zai/xiaomi/deepseek) all
benefit.
2026-05-23 23:52:02 +01:00
Peter Steinberger
6394dd1ac5 fix: preserve gateway lifecycle error cleanup (#85256) (thanks @samzong) 2026-05-23 23:50:55 +01:00
samzong
bc2d501b1d fix(gateway): preserve lifecycle cleanup
Signed-off-by: samzong <samzong.lu@gmail.com>
2026-05-23 23:50:55 +01:00
samzong
9d56f4aa14 fix(gateway): preserve deferred lifecycle errors 2026-05-23 23:50:55 +01:00
Peter Steinberger
4cc2b293db ci: mount local installer scripts in smoke containers 2026-05-23 23:43:36 +01:00
Peter Steinberger
b52c31fe0e fix: speed up agent tool tests 2026-05-23 23:38:11 +01:00
Peter Steinberger
4314674054 perf: reuse plugin metadata snapshots (#85843)
* perf: reuse plugin metadata snapshots

* test: update plugin metadata snapshot mocks
2026-05-23 23:34:19 +01:00
Tyler Bea
45fbf2d81a fix(channels): honor /verbose in group sessions (#85488)
* codex: honor verbose in group dispatch

* codex: address group verbose review findings

Record the final local review pass for the group /verbose PR.

Codex review against origin/main completed clean after tightening the shared group progress gate, keeping public plugin hook types stable, preserving ACP hidden tool boundaries, and adding regressions for live verbose gating and progress-callback suppression.

* codex: require explicit group verbose progress

Normal group tool/progress summaries now require an explicit session verbose override instead of inherited agent verbose defaults.

This addresses the PR review concern that existing verboseDefault configurations could expose group progress after upgrade. DMs and forum-topic behavior continue to use the effective verbose state, while normal groups use the live explicit session verbose state set by /verbose on|full|off.

* codex: document Slack group verbose caveat

* fix(channels): simplify verbose progress gating

* docs(changelog): note verbose channel fix

* fix(channels): preserve quiet default for group progress

* fix(channels): keep verbose error policy dynamic

* fix(channels): default verbose progress off everywhere

* fix(channels): keep followup verbose default quiet

* fix(channels): latch visible tool-error progress

* fix(channels): track failed verbose progress events

* fix(channels): latch delivered tool errors

* fix(channels): prevent progress opt-out bypass

* fix(channels): isolate followup error warning state

* fix(channels): keep full verbose followup warnings

* fix(channels): latch tool errors after visible progress

* fix(channels): require visible followup failure progress

* fix(channels): refresh followup verbose state

* fix(channels): honor live verbose for error details

* test(channels): expect live verbose off warning mode

* fix(channels): preserve static tool error suppression semantics

* fix(channels): bypass acp for colon verbose commands

* fix(channels): narrow dynamic tool warning override

* fix(channels): gate compaction notices on live verbose

* fix(channels): suppress quiet followup compaction callbacks

* fix(channels): suppress tts for hidden tool summaries

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-05-23 23:14:11 +01:00
Peter Steinberger
2cd73d4c89 chore: sync autoreview skill snapshot 2026-05-23 23:10:40 +01:00
Peter Steinberger
1b68dbe95a test: isolate Codex replay timeout outcome 2026-05-23 23:06:41 +01:00
Peter Steinberger
32a80d9954 test: isolate Codex hook channel context 2026-05-23 22:55:52 +01:00
Peter Steinberger
f6204d081f test: isolate Codex duplicate terminal diagnostics 2026-05-23 22:45:19 +01:00
Peter Steinberger
fa5c8345f3 test: isolate Codex terminal diagnostic fallback 2026-05-23 22:34:39 +01:00
Jason (Json)
f603fa58fe fix(discord): keep forced voice consult diagnostics private (#84411)
Summary:
- The PR removes forced consult diagnostics from Discord and phone-call realtime consult payloads, adds private debug logs and regression tests, and records the fix in the changelog.
- Reproducibility: yes. by source inspection. Current main builds the forced Discord consult message with the  ... gent_consult` diagnostic string, and the phone-call fallback passes the same diagnostic as consult context.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(discord): log forced consult fallback reason
- PR branch already contained follow-up commit before automerge: fix(discord): keep forced voice consult diagnostics private

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

Prepared head SHA: c1592530c6
Review: https://github.com/openclaw/openclaw/pull/84411#issuecomment-4494164784

Co-authored-by: FullerStackDev <263060202+fuller-stack-dev@users.noreply.github.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
2026-05-23 21:33:23 +00:00
Peter Steinberger
a705a9c911 test: drain Codex app-server attempts 2026-05-23 22:24:25 +01:00
Gio Della-Libera
05c6e7a553 feat(agents): expose estimated context budget status
Expose a path-free estimated context budget status on session entries and gateway session rows, render it in status when fresh provider usage is unavailable, and clear stale estimates across reset, refresh, compaction, and session-rotation boundaries.

Verification: focused local Vitest covered session persistence, status rendering, gateway rows, model resets, compaction, and session rotation; GitHub CI passed on clean head cad199e43d.

Refs #80594, #54996, #77992, #84490, #83177, #43009, #83526, #8635.
2026-05-23 14:17:44 -07:00
Peter Steinberger
cd102efb70 test: isolate Codex native item release guard 2026-05-23 22:13:22 +01:00
Peter Steinberger
38e1654e09 fix: route Codex image API keys through OpenAI 2026-05-23 22:05:07 +01:00
Kevin Lin
5fbaf2a8a2 feat(whatsapp): support thumb approval reactions (#85477)
* feat(whatsapp): support emoji approval reactions

* fix(whatsapp): simplify approval resolved text

* fix(whatsapp): gate approvals on forwarding config

* ci: ignore injected secrets helpers in oxlint

* fix(whatsapp): use thumb reactions for approvals

* ci: keep secret helpers linted

* fix(approvals): preserve plugin turn source routes

* docs(approvals): remove whatsapp exec approval field refs
2026-05-23 13:58:00 -07:00
Peter Steinberger
6a3781dd7f test: isolate Codex terminal batch scheduler 2026-05-23 21:57:34 +01:00
Peter Steinberger
4c210e22fa Adapt image compression quality by model (#85742)
* feat: adapt image compression quality

* refactor: move image limits into model metadata

* test: cover adaptive image downscaling

* test: cover image tool live providers

* fix: apply media metadata to all image paths

* fix: align providerless image compression

* fix: add chutes runtime image limits

* fix: optimize image data urls with model limits

* fix: type media metadata merge

* fix: optimize data url byte limits after decode

* fix: preserve data url optimizer fallback

* fix: keep low-side image compression fallbacks

* fix: enforce data url image compression policy

* fix: preserve gif data url media policy

* fix: satisfy adaptive image type checks

* test: keep cron provider-runtime mock current
2026-05-23 21:45:55 +01:00
Peter Steinberger
00388134c4 test: isolate Codex terminal release decision 2026-05-23 21:31:17 +01:00
Peter Steinberger
c4f0da00a9 refactor: use channel target resolution APIs (#85814)
* refactor: use channel target resolution apis

* refactor: satisfy delivery lint

* refactor: remove unused target parsing shim

* fix: preserve routed cron topic targets
2026-05-23 21:26:55 +01:00
Kaspre
fd2a9adbe6 fix(ollama): bypass managed proxy for loopback embeddings (#85707)
* fix(ollama): bypass proxy for local embeddings

* fix(ollama): keep managed proxy bypass loopback-only

* fix(ollama): keep proxy bypass internal

* fix(ollama): keep proxy bypass private

* fix(ollama): harden internal proxy bypass

* chore(plugin-sdk): refresh api baseline

* fix(ollama): keep internal bypass out of qa aliases

* test(ollama): keep ssrf runtime mock complete

* fix(ollama): keep dist sdk aliases public-only

* fix(ollama): keep fetch bypass out of infra runtime

* fix(ollama): preserve packaged private sdk alias

* test(ollama): harden private ssrf alias coverage

* test(ollama): cover private ssrf resolver edges

* fix(ollama): scope private sdk native aliases

* test(ollama): audit blocked loopback bypasses

* fix(plugins): keep staged sdk aliases public-only

* test(ollama): harden proxy bypass proof

* test(ollama): cover origin mismatch proxy path

* test(ollama): cover ipv6 and batch bypass paths

* fix lint findings in Ollama proxy tests

* refactor: tighten Ollama proxy bypass

* fix: widen private sdk owner registry type

* test: stabilize Ollama proxy PR checks

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-05-23 21:17:55 +01:00
Peter Steinberger
f6b332c735 test: make Codex diagnostic test deterministic 2026-05-23 21:13:41 +01:00
pashpashpash
8ede81af66 fix(image): hint safe provider timeout (#85812) 2026-05-23 13:10:03 -07:00
Peter Steinberger
2656f13ff8 docs(skills): require autoreview before bug sweep URLs 2026-05-23 20:51:46 +01:00
Peter Steinberger
6fc9d7b14f test: bound Codex app-server cleanup waits 2026-05-23 20:48:03 +01:00
sallyom
266f38b261 fix(control-ui): restore light select arrows
Signed-off-by: sallyom <somalley@redhat.com>
2026-05-23 15:43:42 -04:00
Linux2010
ae79e6e5ec fix(web-ui): improve light theme visibility for select arrows and bubble hover
Problem:
- Select dropdown arrow uses hardcoded #888 SVG stroke, barely visible on
  light backgrounds
- Chat bubble hover border uses 28% accent blend, too subtle in light theme
  for meaningful visual feedback

Fix:
- Darken dropdown arrow SVG to #444 in light theme (.cfg-select)
- Increase chat-bubble:hover accent blend from 28% to 48% in light theme
- Add subtle box-shadow on bubble hover for clearer feedback

Fixes: #85713
2026-05-23 15:43:42 -04:00
Peter Steinberger
d2e9f91cec test: align full release dispatch assertion 2026-05-23 20:24:30 +01:00
Vincent Koc
353d13248e fix(scripts): route check stages through managed runner 2026-05-23 21:20:52 +02:00
Peter Steinberger
9cef99f184 test: clean up Codex app-server run failures 2026-05-23 20:12:44 +01:00
Peter Steinberger
ee61f79b90 ci: retry release child workflow dispatch 2026-05-23 19:56:23 +01:00
Peter Steinberger
071c3e364b test: isolate Codex report snapshot tests 2026-05-23 19:48:06 +01:00
Peter Steinberger
edbd833351 perf(gateway): reduce startup filesystem probes 2026-05-23 19:38:53 +01:00
Peter Steinberger
fcb9c46af0 ci: retry GHCR docker login 2026-05-23 19:28:03 +01:00
Peter Steinberger
d42bc0b684 ci: harden manual checkout auth 2026-05-23 19:11:13 +01:00
Vincent Koc
208a0679e2 fix(scripts): avoid Windows shell argv warnings 2026-05-23 20:00:24 +02:00
Peter Steinberger
02b1c8c902 ci: fix release reachability auth 2026-05-23 18:59:14 +01:00
Peter Steinberger
388b24a34f docs: note docs publishing routing 2026-05-23 18:57:47 +01:00
Peter Steinberger
41f4605020 ci: harden release package validation 2026-05-23 18:48:17 +01:00
Peter Steinberger
3e14f54ffc ci(testbox): expose stable pnpm through corepack 2026-05-23 18:32:04 +01:00
Peter Steinberger
1f2d8f98ba ci(testbox): avoid ready raw runners after hydration failure 2026-05-23 18:30:37 +01:00
Peter Steinberger
f1226aeb6c perf(gateway): defer startup-idle runtime work 2026-05-23 18:27:04 +01:00
Peter Steinberger
391f29baad ci: harden beta release validation flakes 2026-05-23 18:23:39 +01:00
Peter Steinberger
86a0502711 test: type codex thread request mocks 2026-05-23 18:03:19 +01:00
Peter Steinberger
85664f8e71 test: avoid codex heartbeat lifecycle timeout 2026-05-23 17:56:26 +01:00
Vincent Koc
8a94e825cd fix(scripts): run Windows check commands through shims 2026-05-23 18:30:14 +02:00
Peter Steinberger
f4b5e58231 fix: aggressively prune retired model catalogs 2026-05-23 17:29:50 +01:00
Jason O'Neal
7fffbf60b0 fix: harden package URL downloads (#85578)
* fix: harden package URL downloads

Guard package acceptance URL downloads with HTTPS-only validation, no embedded credentials, private/special-use DNS and IP rejection, manual redirect checks, bounded timeout/size limits, pinned lookup, and atomic temp-file writes. Add tooling tests for unsafe URLs, redirect validation, size limits, and successful writes.

* fix: cancel redirect response bodies before closing dispatcher

ClawSweeper P2: the redirect branch in openPackageDownloadResponse cleared
the timeout and awaited dispatcher.close() without first cancelling
response.body. Undici's close() is graceful — it waits for in-flight
requests to complete — so a malicious redirect with a slow/never-ending
body could hang the hardened downloader.

Fix: call response.body?.cancel() before dispatcher.close() to abort the
redirect body immediately.

Test: add a regression test that uses a ReadableStream with an indefinite
interval to simulate a hanging body, and asserts cancel() was called.

Refs: clawsweeper review on PR #85512

* test: harden redirect body cancellation race in regression test

Guard the ReadableStream controller.enqueue() call with a cancelled
flag and try/catch to prevent ERR_INVALID_STATE when the interval
fires after cancel() closes the controller.

* fix: cancel final response body before closing dispatcher in downloadUrl

ClawSweeper P2: the HTTP-error and declared-oversize early-exit paths
in downloadUrl threw before consuming or canceling response.body. The
finally block then cleared the timeout and awaited graceful
dispatcher.close() with the body still open, allowing a slow/never-ending
response to hang release tooling.

Fix: add response.body?.cancel() in the finally block before
dispatcher.close().

Tests: add two regressions:
- HTTP 500 with slow body: asserts cancel() called before dispatcher close
- Declared content-length oversize with slow body: same assertion

* fix: add trusted package URL source policy

* fix: keep package URL resolver dependency-free

* test: cover encoded IPv6 package URL bypasses

* docs: sync package acceptance source overview

* docs: restore release doc formatting

* docs: sync package acceptance trusted-url source

* test: cover dotted IPv4 embedded IPv6 package URLs

* fix: parse dotted IPv4 embedded in IPv6 package URLs

* test: isolate anthropic pruning defaults

* test: move anthropic dated model coverage

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-05-23 17:28:29 +01:00
Peter Steinberger
35969ff440 ci: retry npm Telegram release dispatch 2026-05-23 17:19:00 +01:00
Peter Steinberger
a04566da11 test: isolate Telegram spooled timeout from stall watchdog 2026-05-23 17:08:00 +01:00
Sebastien Tardif
9dc1afe9bb fix(exec-approvals): add .catch() to expiry delivery fire-and-forget (#83106)
* fix(exec-approvals): add .catch() to expiry delivery fire-and-forget

When exec-approval expiry fires, deliverToTargets is called as a
fire-and-forget promise with no .catch(). If delivery fails, the
unhandled rejection swallows the error and the notification is lost.

Add .catch() with log.warn to match the ackDelivery error handling
pattern. Keep pending.delete() before the await (the entry is expired
regardless of delivery success).

Closes #83113

Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>

* fix(approvals): label expiry delivery errors by kind

---------

Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-05-23 16:56:58 +01:00
davidbennett1979
983a3b94c9 fix(memory-core): avoid double bulleting promoted snippets (#85724) 2026-05-23 16:56:06 +01:00
Joey Frasier (Boothe)
ec65b71f5e fix(doctor): skip empty entries and memoize routes in plugin session repairs (#85718)
* fix(doctor): skip empty entries and memoize routes in plugin session repairs

runPluginSessionStateDoctorRepairs called resolveConfiguredDoctorSessionStateRoute
once per session-store key, even for entries that carry no plugin route state
fields. On stores with many CLI sessions (observed ~800 entries), each call
takes ~1.5s due to resolveAgentHarnessPolicy walking config and provider
metadata, so the doctor's state-integrity contribution hangs for minutes
and the surrounding 'openclaw doctor' run effectively never completes.

scanEntryForOwner can only produce repair/manual-review findings when the
entry exposes one of the fields covered by entryMayContainPluginSessionRouteState
(providerOverride/modelOverride/agentHarnessId/cliSessionBindings/etc.), so
the route resolution for empty entries was pure waste. The route itself is
also a function of agentId (sessionKey is only used to derive agentId), so
sessions sharing an agent can reuse one resolved route.

Filter the store by entryMayContainPluginSessionRouteState before resolving,
and memoize resolveConfiguredDoctorSessionStateRoute by agentId within the
remaining entries. On the repro store this drops the contribution from
'never completes' to <100ms.

Adds a guard test that builds a 200-entry store with 2 route-state-carrying
entries and asserts (a) the repair fires exactly once on the codex owner
and (b) the run completes in under 2s (pre-fix would take >5 minutes).

* fix(doctor): skip manifest model-id normalization in plugin session repairs

After the previous filter+memoize fix, runPluginSessionStateDoctorRepairs was
still ~38s on a 230-entry store because every scanned entry calls parseModelRef
on its runtime model. That implicitly enters manifest-driven model-id
normalization via normalizeStaticProviderModelId, which calls
loadPluginMetadataSnapshot when no current snapshot is bound to process state.

loadPluginMetadataSnapshot is filesystem-heavy and is only memoized when a
'current' snapshot is bound (it is not, during doctor), so each parseModelRef
call paid ~40ms of fresh plugin-metadata loading. 672 calls × ~40ms = ~27s
of doctor wall-clock, all of it useless for doctor's purposes: the scan only
needs the normalized provider id of the configured runtime/route to compare
against an owner's providerIds, never the manifest-normalized model id.

Pass allowManifestNormalization: false alongside the existing
allowPluginNormalization: false on all three parseModelRef call sites in
this file. normalizeStaticProviderModelId short-circuits to
normalizeBuiltInProviderModelId when allowManifestNormalization is false,
which is what doctor wants here.

On the same 230-entry store doctor:state-integrity drops from ~38s to ~2.4s
and total openclaw doctor wall-clock drops from ~91s to ~56s.
2026-05-23 16:55:35 +01:00
Peter Steinberger
6191750deb ci: avoid duplicate release-check auth headers 2026-05-23 16:55:03 +01:00
Peter Steinberger
b6530beb05 fix: prune retired model catalog entries 2026-05-23 16:46:59 +01:00
Peter Steinberger
0c192e2915 ci: authenticate release-check reachability fetches 2026-05-23 16:45:13 +01:00
Peter Steinberger
c5f1344faf docs(changelog): note Telegram attachment action fix 2026-05-23 16:43:23 +01:00
Peter Steinberger
054002529d refactor(telegram): simplify action media sends 2026-05-23 16:43:23 +01:00
Keshav's Bot
fdf01db62b fix(telegram): send attachment paths as media 2026-05-23 16:43:23 +01:00
Gio Della-Libera
c897384ae9 fix(doctor): canonicalize git checkout detection (#85735) 2026-05-23 08:42:23 -07:00
Peter Steinberger
030b7bb4b7 test(ci): update plugin prerelease checkout expectation 2026-05-23 16:31:34 +01:00
Peter Steinberger
d9f73cfe33 ci: persist checkout credentials for release validation 2026-05-23 16:17:24 +01:00
Peter Steinberger
5e8c71bf9f test(codex): avoid searchable-tool registration flake 2026-05-23 16:03:45 +01:00
Gio Della-Libera
056378efd5 refactor: simplify doctor repair checks (#83753) 2026-05-23 07:55:12 -07:00
Peter Steinberger
24de3047e5 docs(changelog): credit landed bug sweep PRs 2026-05-23 15:50:38 +01:00
Will.hou
bf84b3089d perf(utils): preserve message identity in stripInlineDirectiveTagsFromMessageForDisplay (#85682)
Consume the existing { text, changed } signal from
stripInlineDirectiveTagsForDisplay so unchanged text-parts keep their
references and the original message is returned when nothing was
stripped. Avoids spurious downstream rerenders/diff churn for consumers
relying on reference equality, and keeps the public SDK helper's text
output and message shape stable.

Fixes #37589.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 15:50:06 +01:00
Sebastien Tardif
49e9c3eb13 fix(agents): add openai-responses family to non-visible turn retry guard (#85603)
openai-codex-responses can return turns where usage.output > 0 but
assistantTexts is empty (hidden reasoning tokens only). The empty
response retry guard only covered openai-completions, anthropic-messages,
and Ollama, so these turns passed through as successful completions
with no content delivered to the user.

Add the full openai-responses API family (openai-responses,
openai-codex-responses, azure-openai-responses, and their transport
variants) to RETRY_GUARD_MODEL_APIS so the empty response and
reasoning-only retry paths can fire for these providers.

Closes #85364

Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
2026-05-23 15:50:01 +01:00
ItsOtherMauridian
6e289b4889 fix(status): show configured cost for aws-sdk models (#85619)
* fix(status): show configured cost for aws-sdk models

Decouple status cost display from provider auth mode so explicit model pricing is used for Bedrock and other non-api-key providers. Include cache read/write tokens in the status cost estimate and cover the behavior with regression tests.

* fix: show configured response usage costs

* docs: align configured cost visibility

* fix(status): keep usage tokens mode cost-free

---------

Co-authored-by: ItsOtherMauridian <165866613+ItsOtherMauridian@users.noreply.github.com>
Co-authored-by: ItsOtherMauridian <itsothermauridian@users.noreply.github.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-05-23 15:49:57 +01:00
Roslin Mahmud Joy
ec43acb432 fix(microsoft-foundry): DeepSeek V4 models incorrectly use openai-completions API (#85549)
When onboarding Microsoft Foundry-hosted DeepSeek-V4 models (Pro/Flash),
the onboarding wizard assigned api: 'openai-completions' because
usesFoundryResponsesByDefault() only matched GPT/o-series models.

These V4 models require the Responses API (openai-responses) to work
correctly against the Foundry endpoint. Without this fix, all calls fail
with 'provider rejected the request schema or tool payload'.

Fix: Add 'deepseek-v4' prefix to usesFoundryResponsesByDefault() so only
the verified V4 family defaults to openai-responses. Older DeepSeek
families (e.g., V3) remain on openai-completions until proven compatible.

Closes: DeepSeek V4 models deployed via Microsoft Foundry onboarding
failing immediately due to wrong API adapter.

Co-authored-by: Roslin <rmj010203@gmail.com>
2026-05-23 15:49:53 +01:00
ANIRUDDHA ADAK
74e65f4d85 fix(skills): show empty state notice in config wizard (#85032)
* fix(skills): show empty state notice when no dependencies to install

* fix(skills): gate empty dependency notice

* fix(skills): tighten all-ready dependency notice

---------

Co-authored-by: Aniruddha Adak <aniruddhaadak80@users.noreply.github.com>
Co-authored-by: Gio Della-Libera <giodl73@gmail.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-05-23 15:49:49 +01:00
Peter Steinberger
ef7e652ec4 test(codex): avoid forced-tool allowlist flake 2026-05-23 15:35:37 +01:00
Peter Steinberger
3e8fd4944f fix: avoid gateway startup event-loop stalls
Defer Gateway channel startup until after readiness, remove startup model prewarm, and move model catalog data onto manifest/static paths so startup no longer loads broad provider runtimes.

Verification:
- focused gateway/catalog/auth/QA Vitest runs
- autoreview clean
- Blacksmith Testbox-through-Crabbox tbx_01ksahn65rsrsqz3q1qyxwf929: pnpm check:changed, exit 0
- PR CI green on ee2b631c72
2026-05-23 15:08:58 +01:00
Peter Steinberger
f6ab188db0 test(codex): type forced-tool request mock 2026-05-23 15:07:49 +01:00
Peter Steinberger
8d1ab83cb3 test(codex): avoid forced-tool turn flake 2026-05-23 14:55:05 +01:00
Peter Steinberger
9ede882f08 test(codex): avoid startup cleanup socket flake 2026-05-23 14:32:13 +01:00
Nyx
2e5be0c7ff fix(gateway): pin relative state dir at startup
* fix(gateway): normalize explicit state dir overrides at startup

* test(gateway): simplify state-dir startup coverage

* test: fix state dir startup coverage

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-05-23 14:30:32 +01:00
Peter Steinberger
b47bace014 fix(whatsapp): persist inbound delivery in plugin state (#85506)
* fix(whatsapp): persist inbound delivery in plugin state

* fix(whatsapp): type durable inbound payload

* fix(channels): close durable receive insert race

* fix(whatsapp): skip owned durable pending duplicates
2026-05-23 14:20:53 +01:00
Peter Steinberger
b4b2ef192d test(codex): make sandbox cleanup proof deterministic 2026-05-23 14:04:12 +01:00
Peter Steinberger
9175491906 fix(cron): route topic targets through channel plugins
Route cron announce topic target parsing through channel plugin target parsers instead of Telegram-specific cron core code. Keep supported Telegram topic forms in the Telegram plugin and document the channel-owned shorthand.
2026-05-23 13:52:06 +01:00
brokemac79
f4b92f5e6c fix(agents): simplify subagent completion handoff
Simplify native subagent completion handoff and remove manual subagent control surfaces.

Co-authored-by: brokemac79 <martin_cleary@yahoo.co.uk>
2026-05-23 13:50:08 +01:00
Peter Steinberger
2ad507c031 fix(release): allow large beta smoke run lists 2026-05-23 13:39:43 +01:00
Peter Steinberger
9c26b87114 ci(release): isolate npm publish concurrency 2026-05-23 13:39:43 +01:00
Peter Steinberger
0e3726305b ci(release): allow beta publish after npm preflight 2026-05-23 13:39:43 +01:00
Peter Steinberger
c689f71805 ci(release): retry child workflow polling 2026-05-23 13:39:43 +01:00
Peter Steinberger
e5dab55aca ci(release): poll child workflows through actions api 2026-05-23 13:39:43 +01:00
zhouhe-xydt
25fa46bd61 fix(bootstrap): guard bootstrap name checks against undefined names (#85523) (#85615)
* fix(bootstrap): guard bootstrap name checks against undefined names

Add optional chaining to isAgentsBootstrapFile and isAgentsBootstrapName
to prevent TypeError: Cannot read properties of undefined (reading 'toLowerCase')
when bootstrap file entries have undefined name properties.

This crash was observed in 2026.5.20 where a workspace bootstrap file entry
with an undefined name caused every incoming message to fail during bootstrap
context building, completely blocking all agent replies.

Fixes #85523

* test(agents): cover unnamed bootstrap truncation entries

* test(agents): keep bootstrap truncation fixture typed

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-05-23 13:37:03 +01:00
Mikael Goderdzishvili
eca9645365 fix(cli): waitForever must keep the event loop alive (#85694)
`waitForever()` is a public library export used by long-running embeds to
block until the host process is asked to exit. It called `interval.unref()`
on the keep-alive timer, which removes the timer from Node's active-handle
set. With no other ref'd handles, `await waitForever()` exits the process
in ~3ms with exit code 13 ("unsettled top-level await") instead of waiting.

Drop the `.unref()` so the interval actually keeps the loop alive, and
update the existing unit test (and comment) to lock in the new contract.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 13:36:56 +01:00
zhouhe-xydt
84229d995a fix(cli-output): ignore cumulative usage from result events in stream-json parser (#85573) (#85625)
* fix(cli-output): ignore cumulative usage from result events in stream-json parser

Claude-cli's stream-json result event reports cumulative cache_read across
all tool sub-calls, not the per-call value. The parser was overwriting the
last assistant-event usage with this inflated sum, causing sessionEntry.totalTokens
to climb 6-13x on tool-heavy turns and trip the preemptive-compaction gate.

Fix: skip reading usage from result events in createCliJsonlStreamingParser,
keeping the last per-call usage from assistant events instead.

Fixes #85573

* fix(agents): keep Claude result usage as fallback

* fix(agents): read Claude assistant stream usage

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-05-23 13:36:51 +01:00
alkor2000
bb52b54134 fix(secrets): show irreversible warning after interactive apply confirmation (#85638)
Fixes #83883.

In `secrets configure`, the one-way-migration irreversibility warning was
computed from `opts.apply` (the original --apply flag) rather than
`shouldApply`. On the interactive path the user confirms "Apply this plan
now?", which sets shouldApply=true while opts.apply stays false, so the
warning was silently skipped and the irreversible plaintext migration was
applied without the second confirmation.

Derive the guard from shouldApply so the irreversibility warning fires on
both the --apply path and the interactive-confirm path. Adds regression
tests covering the interactive path (warning shown; declining it cancels
the apply).
2026-05-23 13:36:47 +01:00
Brian Potter
2c3b7eaa7e fix(agents/harness): pass CLI runtime aliases through to PI in selectAgentHarnessDecision (#85631)
* fix(agents/harness): pass CLI runtime aliases through to PI in selectAgentHarnessDecision

When a model defines `agentRuntime.id` as a CLI runtime alias
(`claude-cli`, `google-gemini-cli`) or a configured `cliBackends` id, the
explicit-non-`auto` branch of `selectAgentHarnessDecision` previously
threw `MissingAgentHarnessError` because the alias has no agent harness
plugin counterpart. Model dispatch is unaffected (the CLI-runtime
short-circuit in `assertModelFallbackCandidateHarnessAvailable` runs
first), but every non-dispatch caller — delivery-mirror metadata
lookups, lane preflight, channel projection — surfaces the throw. On
Slack `[[reply_to:]]` deliveries the warning text gets substituted into
the assistant message synthesized as `provider: openclaw,
model: gateway-injected`, poisoning the thread.

Mirror the existing implicit-codex escape hatch in the same function:
when the runtime is a CLI alias (`isCliRuntimeAlias`) or a configured
CLI backend (`isCliProvider`), return PI with the new
`selectedReason: "cli_runtime_passthrough_pi"`. Actual CLI dispatch is
already routed by callers that consult model runtime policy, so PI here
is just a transcript-composition placeholder — non-CLI typos still
throw as before.

Refs #85582.

* fix(agents): validate CLI harness aliases by provider

* fix(agents): keep custom CLI harness ids fail-closed

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-05-23 13:36:41 +01:00
Peter Steinberger
3c6bc5f0b0 docs: add bug-sweep changelog entries 2026-05-23 13:36:30 +01:00
Vincent Koc
1f32a4855a fix(release): run npm shims on Windows 2026-05-23 14:08:48 +02:00
Vincent Koc
0d7d99befa fix(ci): repair crabbox hydrate replay (#85706) 2026-05-23 20:02:07 +08:00
Daniel Marta
4ec85762ab feat(auth): support named model login profiles
* docs(auth): document named OAuth profile logins

* feat(auth): support --profile-id in models auth login

* docs: note named model login profiles

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-05-23 12:44:57 +01:00
NianJiu
55a0c9b1f4 fix(gateway): restore WebChat image understanding routing
Restores WebChat image uploads to the media-understanding flow without one-turn model overrides.

- removes image-model override plumbing from the reply run
- stages WebChat images as MediaPaths for enrichment
- avoids replaying already-understood images to text-only reply models while preserving undescribed images

Co-authored-by: NianJiuZst <3235467914@qq.com>
2026-05-23 12:31:43 +01:00
haoyu-haoyu
353dfeb108 fix(anthropic): migrate 1M context to GA handling
* feat(anthropic): migrate 1M context from beta to GA

Anthropic has graduated the 1M context window from beta to GA.
This commit:

- Stops injecting the context-1m-2025-08-07 beta header when
  context1m: true is configured
- Removes the OAuth token skip logic that was needed because
  Anthropic previously rejected the context-1m beta with OAuth auth
  (OAuth now supports 1M natively)
- Strips the legacy beta header from user-configured anthropicBeta
  arrays to prevent sending a stale header
- Removes the now-unused isAnthropic1MModel helper,
  ANTHROPIC_1M_MODEL_PREFIXES constant, and logger import from
  the stream wrappers

The context1m config param continues to be respected for context
window sizing in context.ts — only the beta header injection is
removed.

Closes #45550 (Phase 1)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(anthropic): migrate 1M context handling to GA

* fix(clownfish): address review for ghcrawl-156721-autonomous-smoke (1)

* fix(anthropic): restrict ga 1m context models

* docs(anthropic): align ga 1m context guidance

* fix(anthropic): normalize ga 1m model metadata

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-05-23 12:29:31 +01:00
Vincent Koc
5c535df0a2 fix(scripts): resolve Crabbox shims on Windows 2026-05-23 13:23:49 +02:00
Vincent Koc
68bcd4e39d test(ci): harden installer smoke coverage 2026-05-23 13:19:12 +02:00
Gio Della-Libera
f7c05dcc9e fix(status): bound deep docker audit probes (#85476)
* fix(status): bound deep docker audit probes

* chore(status): defer changelog entry to landing

* docs(changelog): note status docker probe timeout

* fix(status): surface Docker probe timeouts

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-05-23 12:08:48 +01:00
Peter Steinberger
a7e0fa08e7 docs: expand meeting notes docs 2026-05-23 11:58:29 +01:00
Jayesh Betala
44d470f7eb fix(cli): validate tasks audit limit (#84901)
* fix(cli): validate tasks audit limit

* docs(changelog): note tasks audit limit validation

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-05-23 11:56:42 +01:00
alkor2000
71ddc016a8 fix(twitch): preserve newer message handler during cleanup (#85425)
* fix(twitch): preserve newer message handler during cleanup

Fixes #83888.

`TwitchClientManager.onMessage` returns a cleanup closure that called
`messageHandlers.delete(key)` unconditionally. When a second onMessage()
for the same account replaced the handler, running the earlier cleanup
deleted the newer handler, leaving the account with no handler and
silently dropping all inbound messages.

Guard the delete with a referential check so the cleanup only removes
the handler it registered. Adds regression tests covering both the
stale-cleanup case (newer handler must survive) and the normal case
(current handler is still removed).

* fix(twitch): distinguish handler registrations

* fix(signal): avoid dangling test export name

* test(meeting-notes): use public sdk imports

* test(sdk): classify meeting-notes subpath

* fix(discord): keep channel entrypoint imports narrow

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-05-23 11:49:01 +01:00
Vincent Koc
1e21121021 fix(ci): require live docker credentials by resource 2026-05-23 12:39:02 +02:00
Lion0710
e0bafc588c fix(diagnostics): drop snake case otel ids (#72645)
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-05-23 18:34:39 +08:00
Vincent Koc
3a1d4dd43f fix(ci): clear signal and docs guard blockers (#85693)
* fix(signal): use lint-safe test api export

* docs: avoid private key sentinel example
2026-05-23 18:27:12 +08:00
Vincent Koc
cc6c3728c7 fix(ci): require factory auth for droid live docker 2026-05-23 12:20:26 +02:00
Peter Steinberger
a4a1abbe30 fix: honor disabled synthetic auth lookup 2026-05-23 11:13:30 +01:00
Vincent Koc
4e34ac483c fix(scripts): repair live docker auth shellcheck 2026-05-23 11:58:16 +02:00
Vincent Koc
5db773fad8 fix(docker): avoid printing gateway token 2026-05-23 11:54:53 +02:00
Vincent Koc
6e3b3183dd fix(cli): keep logs follow on live gateway state
Use the passive backend Gateway client for implicit local logs reads, and route Linux follow-mode local RPC failures to a bounded/redacted active systemd journal fallback instead of stale configured-file logs.

Fixes #83656
Fixes #66841
2026-05-23 17:54:23 +08:00
Vincent Koc
15d9134fc6 fix(e2e): prefer x64 MinGit on Windows 2026-05-23 11:46:25 +02:00
Peter Steinberger
07694c639d feat: add meeting notes plugin
Adds source-only external meeting notes plugin, SDK source-provider contract, CLI access, date-sharded storage, and Discord voice source.
2026-05-23 10:38:09 +01:00
Peter Steinberger
9e55383c3f docs: update changelog for memory artifacts (#85060) (thanks @brokemac79) 2026-05-23 10:32:59 +01:00
brokemac79
e6288cab9a fix(memory): preserve sidecar capability hooks 2026-05-23 10:32:59 +01:00
brokemac79
aac1abeaff fix(memory-lancedb): expose public memory artifacts 2026-05-23 10:32:59 +01:00
Vincent Koc
6657b493e2 fix(e2e): scrub Windows update config on PowerShell 5.1 2026-05-23 11:24:36 +02:00
Peter Steinberger
2c536a8626 docs: absorb documentation PR sweep 2026-05-23 10:23:34 +01:00
Vincent Koc
6b04170167 fix(agents): stabilize Linux fallback tests 2026-05-23 11:10:18 +02:00
Vincent Koc
bcf756ce36 fix(codex): preserve native web search action metadata (#85378) 2026-05-23 17:06:01 +08:00
Peter Steinberger
492d656d74 test: refresh Codex prompt snapshots 2026-05-23 09:56:44 +01:00
Peter Steinberger
99a1107b61 docs: absorb hook and subagent guidance PRs 2026-05-23 09:47:37 +01:00
Vincent Koc
09dd051e78 fix(agents): audit tool policy blocks (#85673)
* fix(agents): audit tool policy blocks

* fix(agents): sanitize tool policy audit fields

* fix(agents): include matched tool policy rule

* fix(agents): bound matched tool policy rules
2026-05-23 16:43:29 +08:00
Peter Steinberger
d485464dbc ci: fix plugin npm bundled dependency install 2026-05-23 09:22:25 +01:00
Gaurav Prasad
558a05b6d0 feat(diagnostics): classify skill and tool usage (#80370)
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-05-23 16:08:55 +08:00
Alex Knight
0b476b9bbb docs(skills): clarify control ui recording proof (#85568) 2026-05-23 17:56:17 +10:00
Vincent Koc
c29967bcc2 test(agents): repair main failure fixtures 2026-05-23 09:53:04 +02:00
samzong
4f0c902012 feat(diagnostics): trace gateway secret preparation (#83019)
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-05-23 15:45:39 +08:00
Vincent Koc
7f05be041e fix(diagnostics): harden observability exports and smokes (#85371)
* test(diagnostics): widen observability smokes

* fix(diagnostics): sanitize observability exports

* docs(diagnostics): format otel export docs
2026-05-23 15:27:43 +08:00
Vincent Koc
0b2ab6c93c fix(stepfun): drop stale auth choice metadata 2026-05-23 09:12:38 +02:00
Vincent Koc
73c1e375e4 test(e2e): sample kitchen sink RSS on Windows 2026-05-23 09:07:31 +02:00
Vincent Koc
c298dfe013 test(plugins): fail gauntlet on load diagnostics 2026-05-23 08:55:44 +02:00
Vincent Koc
9ff1a4371f fix(build): preserve tsdown heap floor 2026-05-23 08:39:17 +02:00
Mason Huang
31c269f0ed fix(tools): honor config apiKey in media tool preflight (#85570)
Summary:
- The branch adds a config-aware tool auth helper, routes image/PDF/media generation preflight and list selection through it, threads `workspaceDir`, and adds focused regression tests plus a changelog entry.
- Reproducibility: yes. by source inspection. Current main gates affected media/PDF/generation preflight paths on env/profile auth while the runtime auth contract already accepts usable `models.providers.*.apiKey`.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(tools): fall back to config apiKey in capability preflight
- PR branch already contained follow-up commit before automerge: fix(tools): honor config apiKey in media tool preflight
- PR branch already contained follow-up commit before automerge: fix(clawsweeper): address review for automerge-openclaw-openclaw-8557…

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

Prepared head SHA: b8c9242d77
Review: https://github.com/openclaw/openclaw/pull/85570#issuecomment-4523770355

Co-authored-by: Mason Huang <masonxhuang@tencent.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: hxy91819
Co-authored-by: hxy91819 <8814856+hxy91819@users.noreply.github.com>
2026-05-23 06:27:03 +00:00
Vincent Koc
b4f62c9afc fix(e2e): support macOS script wrappers 2026-05-23 08:23:31 +02:00
Vincent Koc
743fd4c9db fix(ci): scope changed shrinkwrap checks 2026-05-23 08:17:56 +02:00
github-actions[bot]
33df3be6ca chore(ui): refresh fa control ui locale 2026-05-23 05:41:25 +00:00
github-actions[bot]
908464bbe8 chore(ui): refresh nl control ui locale 2026-05-23 05:41:17 +00:00
github-actions[bot]
62b75f44e0 chore(ui): refresh vi control ui locale 2026-05-23 05:41:05 +00:00
github-actions[bot]
fc4ba31958 chore(ui): refresh th control ui locale 2026-05-23 05:40:51 +00:00
github-actions[bot]
5b1bdd1af8 chore(ui): refresh id control ui locale 2026-05-23 05:40:46 +00:00
github-actions[bot]
534d4b142e chore(ui): refresh pl control ui locale 2026-05-23 05:40:42 +00:00
github-actions[bot]
055c3bd6a5 chore(ui): refresh uk control ui locale 2026-05-23 05:40:21 +00:00
github-actions[bot]
89c5a68951 chore(ui): refresh ar control ui locale 2026-05-23 05:40:11 +00:00
github-actions[bot]
44ca805650 chore(ui): refresh it control ui locale 2026-05-23 05:40:09 +00:00
github-actions[bot]
933b53bf55 chore(ui): refresh tr control ui locale 2026-05-23 05:40:07 +00:00
github-actions[bot]
2240b0e77b chore(ui): refresh fr control ui locale 2026-05-23 05:39:41 +00:00
github-actions[bot]
5fa250b2ed chore(ui): refresh ko control ui locale 2026-05-23 05:39:36 +00:00
github-actions[bot]
f4ea401ccf chore(ui): refresh ja-JP control ui locale 2026-05-23 05:39:30 +00:00
github-actions[bot]
751dde052c chore(ui): refresh es control ui locale 2026-05-23 05:39:27 +00:00
github-actions[bot]
72a9b5b9bc chore(ui): refresh de control ui locale 2026-05-23 05:39:03 +00:00
github-actions[bot]
501b6e075a chore(ui): refresh pt-BR control ui locale 2026-05-23 05:39:00 +00:00
github-actions[bot]
58aa908660 chore(ui): refresh zh-CN control ui locale 2026-05-23 05:38:59 +00:00
github-actions[bot]
88dee79270 chore(ui): refresh zh-TW control ui locale 2026-05-23 05:38:50 +00:00
Kevin Lin
5656f687c1 Add Slack approval QA checkpoints (#85141)
* test: add slack approval qa checkpoints

* fix(slack): scope plugin approval session fallback

* ci(mantis): allow slack approval checkpoint dispatch

* ci(mantis): use on-demand aws slack desktops

* ci(mantis): run slack smoke from candidate checkout

* ci(mantis): pin aws ssh ingress to runner

* test(mantis): skip crabbox actions hydrate for slack desktop

* ci(mantis): use fresh pr checkout for slack desktop

* ci(mantis): start slack desktop smoke from source

* fix(mantis): use relative slack qa output dir

* test(mantis): surface slack smoke failure logs

* fix(mantis): write slack approval watcher script

* fix(mantis): accept successful slack qa metadata

* fix(mantis): tighten slack approval evidence

* fix(mantis): repair slack evidence manifest

* fix(mantis): render slack approval checkpoint proof

* fix(mantis): quote approval checkpoint renderer html

* fix(mantis): preserve slack approval failure artifacts

* fix(mantis): timeout silent slack desktop runs

* fix(mantis): keep slack desktop runs chatty

* fix(mantis): keep slack workflow harness trusted

* fix(qa-lab): make slack approval evidence robust

* fix(qa-lab): harden slack approval workflow proof

* test(qa-lab): surface slack approval diagnostics

* test(qa-lab): loosen slack approval readiness
2026-05-22 22:04:15 -07:00
Gio Della-Libera
d7a078f196 fix(agents): mirror internal ui message tool replies (#85564)
* fix(agents): mirror internal ui message tool replies

* test(tui): prove internal source reply rendering

* fix(agents): preserve source reply idempotency
2026-05-22 21:21:14 -07:00
Dallin Romney
463929d794 perf(whatsapp): narrow runtime setter entry (#85589) 2026-05-22 20:59:21 -07:00
Alex Knight
bb5abefcf5 fix: smooth chat focus mode layout
Collapse the focused chat chrome, suppress focused-mode header scroll churn, and seed the mock chat UI with enough history to exercise scrolling.
2026-05-23 13:52:02 +10:00
Josh Avant
b7450820a9 Fix Telegram missing harness spool poison (#85605)
* fix telegram spool missing harness poison

* docs changelog telegram spool poison
2026-05-22 20:45:44 -07:00
clawsweeper[bot]
679a46d01e fix(session): surface previous-transcript archive failures on /new rotation (#81984) (#85586)
Summary:
- Adds an optional archive-error callback for session transcript archiving, wires `/new` reset rotation to log previous-transcript archive failures, adds regression coverage, and updates the changelog.
- Reproducibility: yes. source-reproducible. Current main catches and ignores `archiveFileOnDisk` failures ins ... and the source PR proof exercises the same rename failure boundary with a real filesystem permission error.

Automerge notes:
- No ClawSweeper repair was needed after automerge opt-in.

Validation:
- ClawSweeper review passed for head 9d5f4c0c70.
- Required merge gates passed before the squash merge.

Prepared head SHA: 9d5f4c0c70
Review: https://github.com/openclaw/openclaw/pull/85586#issuecomment-4523917139

Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
2026-05-23 03:27:48 +00:00
Gio Della-Libera
a94f3444a0 Policy: add agent workspace conformance checks (#85096)
* feat(policy): add agent workspace conformance

* chore(policy): refresh agent workspace checks

* fix(policy): require enabled sandbox for workspace policy

* fix(policy): align agent workspace evidence with runtime
2026-05-22 20:24:31 -07:00
Vincent Koc
2edd6e2462 fix(installer): fail failed Windows git builds 2026-05-23 11:08:04 +08:00
Josh Avant
e0405ecc9b fix(codex): honor node exec policy for native surfaces (#85534)
* fix(codex): honor node exec policy for native surfaces

* docs(changelog): note codex node exec policy fix

* fix(codex): keep node exec policy private
2026-05-22 20:01:29 -07:00
Vincent Koc
304ff68c79 fix(qa-lab): stabilize codex runtime parity fixtures 2026-05-23 10:16:22 +08:00
Dallin Romney
6b52dff22d fix(github): preserve sufficient proof against negative relabel (#85567) 2026-05-22 19:13:33 -07:00
joshavant
5ca734ff8a docs: add changelog for context pressure preflight 2026-05-22 18:46:04 -07:00
Jason (Json)
c08400ea7d Fix context pressure preflight for tool-heavy sessions (#85541)
* fix context pressure preflight for tool payloads

* fix codex rendered context preflight

---------

Co-authored-by: joshavant <830519+joshavant@users.noreply.github.com>
2026-05-22 18:39:37 -07:00
Gio Della-Libera
959b935f3d fix(doctor): classify codex asset notice as info (#85119) 2026-05-22 18:28:45 -07:00
Alex Knight
fe121632ba fix(ui): keep chat picker search current (#85547)
* fix(ui): keep chat picker search current

* test(ui): expand chat picker mock data

* test(openai): satisfy video buffer test typing

* fix(ui): avoid duplicate chat picker search on blur
2026-05-23 11:23:07 +10:00
Shakker
f022b056bd fix: preserve message-tool delivery evidence 2026-05-23 02:22:04 +01:00
Josh Avant
f2365053d3 fix(codex): add API key paste auth (#85533)
* fix codex api key auth paste

* changelog for codex api key auth

* support piped codex api key auth

* fix codex auth prompt validator type

* normalize pasted codex auth secrets

* honor codex auth profile type at runtime
2026-05-22 17:53:05 -07:00
Shakker
743caedb05 fix: satisfy openai video test typecheck 2026-05-23 01:30:16 +01:00
Shakker
6c3fcb8bfc fix: route openai video edits to edits endpoint 2026-05-23 01:27:06 +01:00
Syu
227b4bffee fix(qmd): normalize direct file collection paths (#65212)
* fix(qmd): normalize direct file collection paths

Port fix from PR #65212 to new package location.

When a QMD custom collection path config entry points directly to a file
instead of a directory, normalize into:
- path = parent directory
- pattern = exact filename

This ensures direct file targets are handled correctly regardless of any
user-supplied glob pattern.

Original commit: 3570aa55a7 (fix/flow-runs-legacy-migration)

* fix(qmd): escape direct file collection patterns

* fix(qmd): escape direct file collection masks
2026-05-22 17:16:53 -07:00
Vincent Koc
58e9628300 fix(testbox): preserve clean sparse checkouts 2026-05-23 07:51:55 +08:00
Gio Della-Libera
ad19dd8691 fix(ui): run ui script through junction paths (#85525)
* fix(ui): run ui script through junction paths

* test(ui): make junction direct-execution test portable

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-05-22 16:50:14 -07:00
Alex Knight
60582b671b docs: fix troubleshooting logs link (#85545) 2026-05-23 09:49:53 +10:00
Alex Knight
d69bcfd933 Revert chat session picker inline search (#85527)
* Revert "fix(ui): keep chat session search inline (#85490)"

This reverts commit 260145374f.

* fix(ui): clear applied chat picker search on empty input

* fix(ui): keep chat picker search current

* fix(ui): scope chat picker svg color fix
2026-05-23 09:39:17 +10:00
Shakker
efbf9f3d46 fix: retry guarded video downloads 2026-05-23 00:35:23 +01:00
Shakker
ed7d99aa0e fix: preserve guarded video operation cleanup 2026-05-23 00:35:23 +01:00
Shakker
31b5145594 fix: thread openai video request policy 2026-05-23 00:35:23 +01:00
Shakker
cc48c34f91 fix: honor openai video provider request network policy 2026-05-23 00:35:23 +01:00
joshavant
c1273342d3 docs(changelog): note heartbeat message-tool fix 2026-05-22 15:57:47 -07:00
Fermin Quant
951bbe67b0 fix: use fs-safe trash for agent delete (#84394) 2026-05-22 15:53:41 -07:00
Neerav Makwana
bd9c78f957 Fix heartbeat message-tool delivery policy (#85357)
* fix(heartbeat): honor message-tool delivery policy

Keep scheduled heartbeat turns aligned with group/channel message-tool visibility and model-specific runtime policy so internal tool errors remain private.

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

* test(heartbeat): cover delivery chat type inference

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: joshavant <830519+joshavant@users.noreply.github.com>
2026-05-22 15:52:57 -07:00
Andy Ye
91d85e70c3 Scope config preflight note suppression (#84439) 2026-05-22 15:51:34 -07:00
Zhaocun Sun
1cd6dce075 fix(cron): document best-effort edit delivery mode (#84526) 2026-05-22 15:49:41 -07:00
Rohit
2e15830d02 fix(dreaming): open report cards from memory palace (#85144) 2026-05-22 15:48:16 -07:00
Luke
49ce171aa5 fix(openai-codex): preserve image input capability (#85393) 2026-05-22 15:46:30 -07:00
Josh Lehman
c52daa4cdf fix(codex): stabilize heartbeat dynamic tool schema (#84681) 2026-05-22 18:45:10 -04:00
Peter Steinberger
658be7f1c7 docs: absorb small documentation PRs 2026-05-22 23:42:51 +01:00
Alex Knight
13a4c57991 fix(scripts): preserve bws resolver parse errors (#85528) 2026-05-23 08:41:55 +10:00
狼哥
f2d4f9328c fix(telegram): honor outbound media max bytes (#83478) 2026-05-22 15:38:54 -07:00
Jackal Xin
1dd3b52cb7 fix(skills): restore executable bit on bundled whisper script + release-time check (#41351)
* Enforce executable shell scripts in bundled skills

* fix: format CONTRIBUTING.md (oxfmt trailing whitespace)

* fix: skip shell script executable check on Windows

Windows does not support Unix permission bits — chmod is a no-op and
statSync().mode never reports execute bits.  Skip the runtime check
and the corresponding tests on win32.

* style: restore contributing formatting

* chore(ci): refresh detect-secrets baseline

* fix(skills): mark video-frames frame script executable

* fix: revert unrelated CI/secrets changes from whisper chmod PR

* chore(ci): retrigger full PR checks

* test: annotate executable-bit regression suite

* test(tts): mock resolveModelAsync in summarizeText tests

* test(whatsapp): make append history test use stale timestamp

* test(models): tolerate registry loader option expansion

* docs: add changelog for bundled skill executable fix

* fix(config): allow partial Codex web search location

* Drop unrelated formatting from PR 41351

* Fix bundled plugin bridge source expectation

* test: restore bundled plugin bridge npm expectation

---------

Co-authored-by: xaeon2026 <xaeon2026@gmail.com>
Co-authored-by: Jackal Xin <jackal092927@users.noreply.github.com>
Co-authored-by: xaeon2026 <xaeon2026@users.noreply.github.com>
2026-05-22 15:37:15 -07:00
ZC
2d5bda9199 fix(google): print Gemini OAuth URL before browser launch (#71469) 2026-05-22 15:35:32 -07:00
Peter Steinberger
b3622beecb docs: absorb contributor documentation fixes
Co-authored-by: ayesha-aziz123 <moizs4644@gmail.com>
Co-authored-by: dishraters <dishraters@gmail.com>
Co-authored-by: hougangdev <devchain7890@gmail.com>
Co-authored-by: Brandon Lipman <brandon@offdeck.com>
2026-05-22 23:28:31 +01:00
Peter Steinberger
8f8638393e docs: tighten landable bug sweep gates 2026-05-22 23:21:35 +01:00
Dallin Romney
299ed80834 fix: reuse provider auth lookup facts (#85499)
* fix: reuse provider auth lookup facts

* test: update model auth mocks

* fix: scope synthetic auth registry lookup
2026-05-22 15:14:04 -07:00
Peter Steinberger
7e1237032b fix: keep session picker focus separate 2026-05-22 23:01:34 +01:00
clawsweeper[bot]
464ffc1003 feat: start onboarding for fresh CLI installs (#85519)
Summary:
- This PR routes bare `openclaw` to classic onboarding for missing, empty, or metadata-only configs; keeps aut ... cs/changelog/tests; and narrows a Docker E2E boundary-check exception for an existing source-checkout lane.
- Reproducibility: not applicable. this is a feature/default-routing PR rather than a bug report. The branch p ... ill includes a fresh-state terminal run reaching `OpenClaw setup` and tests for the relevant config states.

Automerge notes:
- PR branch already contained follow-up commit before automerge: feat: start onboarding for fresh CLI installs

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

Prepared head SHA: f4b2572f2e
Review: https://github.com/openclaw/openclaw/pull/85519#issuecomment-4522938004

Co-authored-by: FullerStackDev <263060202+fuller-stack-dev@users.noreply.github.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
2026-05-22 22:00:21 +00:00
Peter Steinberger
64d13c017a docs: refresh contributor docs
Co-authored-by: Quratulain-bilal <umayaimanshah@gmail.com>
Co-authored-by: Mariano Belinky <mbelinky@gmail.com>
Co-authored-by: tao <itaofe@gmail.com>
Co-authored-by: julian <julian@tencent.com>
Co-authored-by: xenouzik <xenouziq@gmail.com>
Co-authored-by: Olamiposi <56056759+posigit@users.noreply.github.com>
Co-authored-by: surlymochan <surlymo@apache.org>
Co-authored-by: Janaka A <contact@janaka.co.uk>
Co-authored-by: choiking <samsamuels1927@gmail.com>
2026-05-22 22:58:27 +01:00
Jason (Json)
84f6b5c7f8 fix(update): prepack npm git update specs
Prepack npm GitHub/git source update specs into temporary tarballs before the staged global npm install. Extends coverage to hosted GitHub HTTPS URLs without a `.git` suffix.

Co-authored-by: fuller-stack-dev <263060202+fuller-stack-dev@users.noreply.github.com>
2026-05-22 22:58:16 +01:00
Peter Steinberger
7e16a50c7e fix: simplify chat session search 2026-05-22 22:54:21 +01:00
Peter Steinberger
0556958d82 fix: use native mac settings sidebar 2026-05-22 22:53:32 +01:00
Peter Steinberger
dd07fb400f chore: ignore antigravity cli state 2026-05-22 22:53:08 +01:00
Peter Steinberger
0622fb6d90 fix(media): replace Gemini CLI fallback with sandboxed Antigravity (#85518)
* fix(media): prefer antigravity over gemini cli fallback

* fix(media): pass antigravity workspace before prompt

* fix(media): keep antigravity prompt argument single-line

* fix(media): sandbox antigravity media fallback

* test(media): isolate antigravity cli override

* fix(media): isolate antigravity capability probe
2026-05-22 22:49:45 +01:00
Peter Steinberger
faad2b0a71 docs: add bugbash landing changelog entries 2026-05-22 22:34:49 +01:00
Zhaocun Sun
9b5c281a3a fix(diffs): continue after card hydration errors (#84775)
* fix(diffs): continue after card hydration errors

* fix(diffs): satisfy bundled extension lint

* fix(diffs): continue after card hydration errors

* fix(diffs): satisfy bundled extension lint

* fix(diffs): keep failed hydration controllers out

* fix(diffs): restore clean current-main diff
2026-05-22 22:33:54 +01:00
Sebastien Tardif
e008bc92c3 fix(proxy): add missing clientSocket error handler in CONNECT tunnel (#82444)
The CONNECT handler pipes clientSocket and upstreamSocket together but
only registers an error handler on upstreamSocket. If the client
disconnects abruptly (ECONNRESET), the unhandled error event on
clientSocket causes the Node process to crash.

Add a clientSocket error handler that logs the event and destroys the
upstream socket. Also change clientSocket.end() to clientSocket.destroy()
in the upstream error handler since destroy() is more appropriate for
error cleanup of piped sockets.

Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
2026-05-22 22:33:46 +01:00
Sergio Cadavid
7134a95c90 fix(memory): report qmd workspace cwd probe failures (#63167) 2026-05-22 22:31:59 +01:00
luna system
bf1a22ced4 fix(agents): handle parallel tool call deltas in openai-completions stream (#82263)
* fix(agents): handle parallel tool call deltas in openai-completions stream

The OpenAI completions streaming parser tracked only a single
`currentBlock` for tool calls and ignored `toolCall.index`. When the
API sends multiple `delta.tool_calls` entries (e.g., parallel tool
call scaffolding from kimi-for-coding), the parser created a new
block for every entry with a differing `id`, spawning phantom tool
calls with empty names and misrouting arguments.

Replace single-block tracking with Maps keyed by `index` and `id`,
matching the correct logic already present in the bundled
`@earendil-works/pi-ai` dependency. This ensures parallel and
interleaved tool call deltas accumulate to the correct block.

Fixes phantom "unknown" tool calls and empty arguments on
parameterized tools for providers that emit multiple tool_call
indices in streaming deltas.

* fix(agents): finalize tool-call blocks in place to keep maps live

ClawSweeper review [P1]: finishCurrentBlock() and finishAllToolCallBlocks()
were creating new block objects and replacing them in output.content,
but toolCallBlocksByIndex / toolCallBlocksById still pointed at the old
objects. Later deltas for those indices would mutate detached blocks,
causing argument loss and incorrect contentIndex in stream events.

Fix by finalizing arguments in place on the existing block objects.

Add regression test for parallel tool calls with split indices:
- two tool-call slots introduced in one chunk
- argument deltas arriving for each index in subsequent chunks

* fix(agents): keep byte counters out of emitted tool-call blocks

ClawSweeper review [P2]: partialArgsBytes was being stored directly on
the tool-call block objects pushed into output.content, exposing parser
scratch state to emitted stream events and final transcripts.

Replace the inline field with a WeakMap keyed by block object, keeping
byte tracking internal to the parser without polluting the public block
shape.

* refactor(agents): extract ToolCallBlock type for map declarations

ClawSweeper review [P1]: NonNullable<typeof currentBlock> at the map
declaration point was unreliable because currentBlock is initialized to
null and flow-narrowed. Define a local ToolCallBlock alias and use it
for toolCallBlocksByIndex, toolCallBlocksById, and toolCallBlockBytes
to give the maps a precise, stable type.

* fix(agents): iterate typed tool-call map in finishAllToolCallBlocks

ClawSweeper review [P1]: output.content elements are typed as
Record<string, unknown>, so block.partialArgs remained unknown even
after checking block.type === "toolCall". Latest CI failed strict
type checking at parseStreamingJson(block.partialArgs).

Fix by iterating toolCallBlocksByIndex.values() instead — the Map
values are already typed as ToolCallBlock, so partialArgs is known
to be a string and parseStreamingJson compiles cleanly.
2026-05-22 22:31:52 +01:00
Dallin Romney
423f525438 test: align release validation package acceptance check (#85515) 2026-05-22 14:30:35 -07:00
Peter Steinberger
44d5330993 fix: recover stuck Codex compaction
- Restart the shared Codex app-server client when native server-side compaction times out.
- Retry native compaction once on the fresh app-server while preserving stale-thread cleanup only for `thread not found`.
- Add regression coverage and changelog entry for the preflight compaction recovery path.

Verification:
- `pnpm test extensions/codex/src/app-server/compact.test.ts`
- `env -u OPENCLAW_TESTBOX -u OPENCLAW_TESTBOX_REMOTE_RUN pnpm check:changed`
- `.agents/skills/autoreview/scripts/autoreview --mode local`

CI note: `build-artifacts` is red due inherited latest-main workflow/test drift, reproduced locally outside this PR diff and tracked in the pre-merge PR comment.
2026-05-22 22:30:06 +01:00
Alex Knight
8174bfc734 docs: require visual proof for control ui e2e (#85513) 2026-05-23 07:24:20 +10:00
Gio Della-Libera
dcc5e45b50 Policy: add gateway exposure checks (#81981)
* feat(policy): add gateway exposure conformance

* fix(policy): align custom bind exposure evidence
2026-05-22 14:18:01 -07:00
Peter Steinberger
dcfc7e58fa ci: unblock advisory Tideclaw alpha release checks 2026-05-22 22:09:18 +01:00
Vincent Koc
684a9b2e6e fix(installer): tolerate WSL UNC launch cwd 2026-05-23 04:59:08 +08:00
Peter Steinberger
bb5010b89a docs: absorb docs sweep
Co-authored-by: Kai <kai@itskai.dev>
Co-authored-by: Weihang <gwh7078@163.com>
Co-authored-by: Scott Long <longstoryscott@gmail.com>
Co-authored-by: moejaberr <mjaber@uoguelph.ca>
Co-authored-by: huihui0822 <109355071+huihui0822@users.noreply.github.com>
2026-05-22 21:52:01 +01:00
Peter Steinberger
60e3749de3 fix: cancel stale provider auth prewarms (#85503) 2026-05-22 21:51:43 +01:00
Dallin Romney
0a50cbdf34 Add TUI PTY integration coverage (#85485)
* test: add TUI PTY integration coverage

* test: stabilize TUI PTY CI

* test: speed up TUI PTY coverage

* test: bound TUI PTY local waits

* ci: keep TUI PTY gate fast

* test: route TUI PTY project in full suite

* ci: run TUI PTY on routing edits
2026-05-22 13:42:58 -07:00
Sebastien Tardif
7bc4a333aa fix(security): escape entry.id in HTML export to prevent attribute XSS (#83104)
* fix(security): escape entry.id in HTML export to prevent attribute XSS

Apply escapeHtmlAttr to entry.id in renderEntry and renderCopyLinkButton
to prevent attribute injection via crafted entry IDs in HTML exports.

Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>

* chore: remove proof helper scripts from branch

ClawSweeper P2: committed proof scripts can provide false-positive
validation. Proof output is in the PR body instead.

Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>

---------

Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
2026-05-22 21:27:14 +01:00
Logan Ye
76a025c2fd fix: guard openai-completions tool payload with supportsTools compat flag (#74738)
* fix: guard openai-completions tool payload with supportsTools compat flag (#74664)

* docs(changelog): note OpenAI completions tool compat fix

* test(agents): use real tool history fixtures
2026-05-22 21:27:06 +01:00
in-liberty420
995a02033d fix(slack): surface auth.test failure + normalize explicit-bot mention check (#85101)
When the Slack adapter's startup auth.test call fails (bad token,
transient error, etc.), the bot user id silently stays empty for the
life of the process. The downstream explicit-bot mention check is
`botUserId && mentionedUserIds.includes(botUserId)`, which always
returns false when botUserId is empty. The result is that explicit
<@bot> mentions are silently classified as non-mentions with no log
trace explaining why.

Changes:
- provider.ts: stop swallowing auth.test failures; emit a warn log at
  boot so the degraded state is observable. Empty user_id is treated
  as a failure too.
- prepare.ts + subteam-mentions.ts: export the existing normalizeSlackId
  helper and apply it to both sides of the explicit-bot equality check
  (and to the mentioned-ids list). Real Slack ids are already uppercase,
  so this is a no-op on healthy traffic, but it locks the invariant down
  and removes the asymmetry between collected ids and the ctx bot id.
- prepare.test.ts: add two regression tests pinning the exact symptom:
  positive case (botUserId set -> explicit_bot), negative case
  (botUserId='' -> not explicit_bot, mention_source not explicit_bot).

🤖 AI-assisted.

Co-authored-by: in-liberty420 <in-liberty420@users.noreply.github.com>
2026-05-22 21:26:59 +01:00
Peter Steinberger
4df34cb790 chore(release): bump version to 2026.5.22 2026-05-22 21:25:16 +01:00
Peter Steinberger
260145374f fix(ui): keep chat session search inline (#85490)
* fix(ui): keep chat session search inline

* fix(ui): tolerate partial chat session search state
2026-05-22 20:52:29 +01:00
Gio Della-Libera
c85feace54 Policy: add secret and auth conformance checks (#81974)
* feat(policy): add secrets auth conformance

* fix(policy): include sandbox ssh secret data

* fix(policy): complete secret input provenance

* fix(policy): cover media request secrets

* fix(policy): satisfy policy lint

* fix(policy): narrow secret conformance evidence

* fix(policy): cover request bearer token secrets
2026-05-22 12:48:14 -07:00
Sebastien Tardif
f75789f803 fix(delivery): log failDelivery errors instead of silently swallowing (#84449)
Replace empty .catch(() => {}) on two failDelivery calls with
log.warn() so delivery queue mark-failed errors leave a diagnostic
trail instead of being silently discarded.

Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
2026-05-22 20:45:26 +01:00
Jayesh Betala
5c866a17d7 fix(cli): validate debug proxy numeric options (#84260) 2026-05-22 20:45:17 +01:00
Andy Tien
501e74ddf3 fix(daemon): use exit code instead of localized text for schtasks fallback (#85347)
* fix(daemon): use exit code instead of localized text for schtasks fallback

Problem:
- shouldFallbackToStartupEntry() only matched English/Spanish error messages
  ("access is denied" / "acceso denegado"), causing silent fallback failure
  on non-English Windows systems (Chinese, Japanese, French, German, etc.)

Fix:
- Replace regex matching with exit code check (params.code === 1)
- schtasks returns exit code 1 for access denied / generic failure
  regardless of system locale

Fixes: #85255

* test(daemon): cover localized schtasks fallback

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-05-22 20:43:40 +01:00
infracore
5c614de29a fix(auto-reply): enforce word boundary in slash command prefix match (#84634)
`parseSlashCommandActionArgs` used a naive `startsWith` against the
configured slash prefix. When a skill name shares a prefix with a
built-in command (e.g. a skill named `config-check` vs the built-in
`/config`), the longer name was captured by the shorter built-in
handler and surfaced as an invalid action:

  ⚠️  /config is disabled. Set commands.config=true to enable.

Any skill whose name starts with a built-in command prefix
(`config-*`, `debug-*`, `models-*`, etc.) was unreachable via slash
invocation from any channel.

Fix: after the prefix match, require that the next character is
whitespace, a colon, or end-of-string. Otherwise the prefix
collided with a longer command name and we return `no-match` so the
longer handler — or the skill router — gets a chance to claim it.

Adds a regression test file `commands-slash-parse.test.ts` covering:
- `/config-check <args>` returns null (the reported case)
- `/configfoo` (no separator) returns null
- `/modelsy` returns null for the `/models` prefix
- `/config:json` still matches (colon is a valid boundary)
- `/config show enabled` still parses cleanly (whitespace boundary)
- empty body still returns the default action

Fixes #84572.

Co-authored-by: infracore <infracore@users.noreply.github.com>
2026-05-22 20:42:22 +01:00
zhang-guiping
63545693a0 fix(message-tool): normalize send body aliases (#84102) 2026-05-22 20:38:08 +01:00
Peter Steinberger
e0fda55cf7 docs: absorb maintainer docs sweep
Co-authored-by: Bob Du <i@bobdu.cc>
Co-authored-by: alitariksahin <alitariksah@gmail.com>
Co-authored-by: Jefsky <hwj3344@hotmail.com>
Co-authored-by: Musaab Hasan <m9.3b@Hotmail.com>
Co-authored-by: Intern Dev <dev@wukongai.io>
Co-authored-by: majin.nathan <majin.nathan@bytedance.com>
2026-05-22 20:29:10 +01:00
Peter Steinberger
d946a02a13 fix(gateway): coalesce provider auth rewarms
Coalesce provider auth-state rewarms after auth-profile failures and include event-loop delay in provider auth warm logs.
2026-05-22 20:28:13 +01:00
狼哥
57178b188b docs(voyage): clarify API key setup (#81803) 2026-05-22 20:20:22 +01:00
吴杨帆
88f50e8cd1 docs(config): quote bracket config paths (#83058) 2026-05-22 20:20:10 +01:00
Riive
14b2b8ac48 docs: link Copilot model availability (#76252) 2026-05-22 20:19:52 +01:00
Peter Steinberger
9fae5f7697 test(telegram): await watchdog registration event 2026-05-22 20:15:53 +01:00
Peter Steinberger
4b63502279 ci: run binding command escape in release checks 2026-05-22 20:12:53 +01:00
Dallin Romney
b741ddb66f fix(tui): dismiss watchdog notice when response actually arrives (#77375)
* fix(tui): dismiss watchdog notice when response actually arrives

The streaming watchdog renders 'This response is taking longer than
expected. Send another message to continue.' after 30s without a chat
delta. If a delta or final then arrives — common for runs that are slow
but not stuck — the notice stays in the log alongside the recovered
response and contradicts what the user sees.

Track the notice by runId in the chat log via a new `addPendingSystem`
+ `dismissPendingSystem` pair (mirroring the existing pendingUsers
pattern) and dismiss it from `handleChatEvent` whenever any further chat
event for that run is processed. The watchdog's internal cleanup
(`activeChatRunId` reset, status idle, history reload) is unchanged.

Refs #67052, #69081 (closed). Prior attempt #69026 raised the threshold
and suppressed the notice entirely; this is the narrower fix that keeps
the warning useful for genuinely stuck runs.

* fix(tui): adapt pending notice to repeatable system entries

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-05-22 20:02:36 +01:00
Peter Steinberger
d756e1c500 test: add docker proof for plugin binding command escape 2026-05-22 19:58:04 +01:00
Peter Steinberger
7c9127c94d test(telegram): wait for polling watchdog deterministically 2026-05-22 19:50:51 +01:00
Peter Steinberger
0241a6e7ae ci: skip pnpm auto repair in Crabbox shell 2026-05-22 19:47:16 +01:00
Sebastien Tardif
99e44f623e fix(gateway): add .catch() to SIGTERM/SIGUSR1 signal handlers (#83131)
The SIGTERM handler's fire-and-forget IIFE can reject if the graceful
drain or tunnel-teardown throws. Without a catch, this becomes an
unhandled promise rejection. Add .catch() that logs the error and
falls back to a hard stop request. Same treatment for SIGUSR1.

Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
2026-05-22 19:47:09 +01:00
yozakura-ava
247e536fa6 fix: release cron runtime state after isolated runs (#85053)
* fix: release cron runtime state after isolated runs

After an isolated cron/subagent run completes, the prepared context retains
references to the full in-memory session store and the registered agent run
context. Over many runs, these retained objects accumulate -- heap snapshots
showed ~2.0 GiB from ~113k copies of the skill prompt string flowing through
skillsSnapshot.prompt -> session entry -> cronSession.store -> cron run context.

Changes:
- Add disposeCronRunContext() to runCronIsolatedAgentTurn's finally block
- Calls clearAgentRunContext(sessionId) to remove the run context from the
  global agent-events map
- Nulls cronSession.store to release the in-memory session registry copy
- Export clearAgentRunContext from run-execution.runtime.ts barrel
- The disposal is shallow O(1) -- no deep traversal, no hot-path disk writes
- Session persistence is unaffected (on-disk sessions.json is untouched)

The finally block guarantees cleanup on both success and error paths,
including timeout/abort scenarios.

Includes unit tests for clearAgentRunContext, store disposal, and
sweepStaleRunContexts.

* fix: remove duplicate storePath property in test fixture

* fix: remove unused clearAgentRunContext import from run-executor

* fix(cron): use initial sessionId for disposeCronRunContext in finally block

finalizeCronRun calls adoptCronRunSessionMetadata() which can rotate
sessionEntry.sessionId before the finally block runs. Capturing the
sessionId before the try block ensures clearAgentRunContext clears the
correct registered context instead of the potentially-rotated one.

Also removes unused imports (vi, beforeEach) from the runtime cleanup test.

* chore: trigger CI re-check for proof gate

* chore: retrigger CI proof gate

* test(cron): prove isolated run cleanup path

* fix(cron): keep shared run contexts active

* test(cron): avoid spreading typed-never fixture

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-05-22 19:46:58 +01:00
Sergio Cadavid
0c7220f5da fix(cron): suppress fatal error completion announce (#83724)
* fix(cron): suppress fatal error completion announce

* fix(cron): preserve cleanup for fatal announce suppression

* test(cron): avoid spreading typed-never announce fixture

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-05-22 19:46:54 +01:00
amittell
34c441c746 fix(exec): parse nested approval metadata in async followups (#72268)
* fix(exec): parse nested approval metadata in followups

(cherry picked from commit 10ff9b318e77cda3d65f40d59bbab0f4a3f59da8)

* docs(changelog): note exec approval nested-paren parser fix

* fix(exec): sanitize denied-reason literals in (...)-delimited approval messages

The exec-approval followup wire format is `Exec denied (gateway id=..., <deniedReason>): cmd`. The producer at `src/agents/bash-tools.exec-host-gateway.ts:606` was emitting `approval-timeout (allowlist-miss)`, which embedded literal parens inside the metadata segment and broke the metadata/body boundary for naive parsers. Switch the literal to a colon-separated form (`approval-timeout: allowlist-miss`) so the surrounding `(...)` delimiter stays unambiguous.

The Gateway node-event surface at `src/gateway/server-node-events.ts:734` interpolates an untrusted `obj.reason` into the same `Exec denied (node=..., <reason>)` format. Strip parens from that field before interpolation so a buggy or hostile node payload cannot smuggle metadata into the body slot.

The robust nested-paren parser already in `src/agents/exec-approval-result.ts` stays as defense in depth. Extend `exec-approval-result.test.ts` to cover the canonical colon-separated `deniedReason` and confirm `formatExecDeniedUserMessage` still maps it to the timeout copy.

* fix(exec): require gateway/node metadata source to reject spoofed approval wrappers

The exec-approval result parser previously accepted any string starting with
"Exec denied (..." or "Exec finished (..." as a structured approval wrapper.
Generic command stdout that happened to start with these tokens would be
classified as kind: "denied" or "finished", letting a tool's output spoof a
resolved-approval event in pi-embedded-subscribe.handlers.tools.ts:1173.

Reported by Aisle as CWE-841 (Improper Enforcement of Behavioral Workflow),
medium severity. The fix validates that the parenthesized metadata starts with
either "gateway id=" or "node=" — both prefixes are emitted by the legitimate
approval generators (bash-tools.exec-host-gateway.ts, bash-tools.exec-host-node.ts,
gateway/server-node-events.ts) and are unlikely to appear in arbitrary command
output. Inputs that fail this check now return kind: "other", which all callers
already handle as a no-op.

* fix(exec): keep sandbox_blocked classification for raw exec-denied messages

After the spoof-guard tightening of parseExecApprovalResultText, inputs that
lack a gateway/node-sourced metadata prefix (such as the synthetic
"exec denied (allowlist-miss):" string used in classifier tests) no longer
return kind: "denied" and therefore no longer trigger formatExecDeniedUserMessage,
so isSandboxBlockedErrorMessage stopped recognising them.

Add a direct \bexec denied\s*\( alternative to SANDBOX_BLOCKED_RE so the
classifier still treats any raw "exec denied (" prefix as sandbox-blocked,
independent of whether the parser accepts the surrounding wrapper. This keeps
classifyProviderRuntimeFailureKind's existing behavior for unstructured exec-
denied messages.
2026-05-22 19:46:49 +01:00
Peter Steinberger
7552634996 ci: share Crabbox hydrate pnpm store 2026-05-22 19:37:46 +01:00
Peter Steinberger
736e7de1ae chore(release): refresh plugin SDK baseline 2026-05-22 19:32:30 +01:00
Peter Steinberger
b6940b5dc4 ci(release): pass node pin to pnpm setup 2026-05-22 19:27:56 +01:00
Peter Steinberger
a26aba67a8 ci(release): harden node setup before pnpm cache 2026-05-22 19:27:56 +01:00
Peter Steinberger
b00d3065cf ci: use stable pnpm wrapper for Crabbox hydrate 2026-05-22 19:25:19 +01:00
Peter Steinberger
86b87df7e3 docs: refine maintainer docs sweep
Co-authored-by: Niels Kaspers <kaspersniels@gmail.com>
Co-authored-by: Zhaocun <zhaocunsun@gmail.com>
Co-authored-by: Henson <zccyman@163.com>
2026-05-22 19:22:40 +01:00
cassthebandit
bd04b1ea7c docs(memory): add guidance for action-sensitive memories (#82788) 2026-05-22 19:21:00 +01:00
oak
d012065ecf docs(feishu): add dynamicAgentCreation and per-user isolation docs (#82793)
Add documentation for the dynamicAgentCreation feature used to create
isolated agents per Feishu/Lark user. Covers:

- dynamicAgentCreation configuration fields (enabled, workspaceTemplate,
  agentDirTemplate, maxAgents)
- Automatic agent/workspace creation flow
- Session isolation with dmScope
- Template variables ({agentId}, {userId})
- Verification steps and example deployment

Refs: feature available since OpenClaw 2026.4.25+

Co-authored-by: li <li@lideMac-mini.local>
2026-05-22 19:20:46 +01:00
alexgduarte
ce5dcb0ab2 docs(secrets): clarify agent-readable plaintext boundary (#84574)
Co-authored-by: alexgduarte <24414784+alexgduarte@users.noreply.github.com>
2026-05-22 19:20:25 +01:00
Dr. Claw
bbbed264b6 docs(channels): document ackReactionScope for Slack & Telegram (DM gotcha) (#84233)
* docs(channels/slack,telegram): document ackReactionScope and its DM-excluding default

The Slack and Telegram channel docs documented `ackReaction` but not
`ackReactionScope`, even though the scope (defaulting to
`group-mentions`) silently excludes DMs. People who set `ackReaction`
and expect to see an emoji on DMs are surprised when nothing fires.

This adds:

- The resolution order for `ackReactionScope` (per-account → channel →
  `messages.ackReactionScope` → default `group-mentions`).
- The full list of scope values (`all`, `direct`, `group-all`,
  `group-mentions`, `off`/`none`).
- A Note callout flagging that the default does not react in DMs and
  that `messages.ackReactionScope` requires a gateway restart to take
  effect.
- A short JSON example for the common case (`ackReactionScope: "all"`).

Mirrors the structure already used in `docs/channels/matrix.md`.

Found while configuring Slack DMs to show `👀` ack reactions and
discovering that the docs covered the emoji but not the scope gate. AI-assisted.

* fixup: scope is messages-only for Slack & Telegram (not per-account)

Reviewer correctly noted that the Slack and Telegram runtimes only read
`cfg.messages?.ackReactionScope` and the per-account/per-channel
`ackReactionScope` keys don't exist in those schemas (only Discord and
Matrix support them). Drop the misleading resolution-order bullets and
document `messages.ackReactionScope` only.

Verified against:
- extensions/slack/src/monitor/provider.ts:243
- extensions/telegram/src/bot-core.ts:262
- src/config/types.slack.ts (no ackReactionScope in account schema)
- src/config/types.telegram.ts (no ackReactionScope in account schema)

Keeps the DM-default gotcha, the full enum, and the gateway-restart note,
which were the original value of the PR.

---------

Co-authored-by: Dr. Claw <drclaw-iq@users.noreply.github.com>
2026-05-22 19:20:10 +01:00
Peter Steinberger
a0702e195d build(pnpm): use packageManager as pnpm source
Recreated from #85108 because the original branch could not be updated by maintainers.

Preserves current-main pnpm install hardening while switching workflow pnpm setup to packageManager, and adds exact version-scoped release-age exclusions for already-locked packages that pnpm 11.2.2 audits during install.

Co-authored-by: Altay <altay@hey.com>
2026-05-22 19:17:43 +01:00
Peter Steinberger
f6840acc21 ci: export Crabbox hydrate pnpm layout 2026-05-22 19:16:33 +01:00
CodeReclaimers
6f416537ee fix(gateway): preserve fresh agent session state
Fixes #5369.

Preserve fresh session-store state when the agent handler observes a stale cached session entry, including model/provider overrides, send policy, delivery metadata, lifecycle timestamps, and fresh session rotations.

Co-authored-by: CodeReclaimers <github@codereclaimers.com>
2026-05-22 19:11:20 +01:00
Vincent Koc
77c3bdb3ca fix(gateway): attribute agent wait timeouts 2026-05-23 02:07:44 +08:00
Peter Steinberger
489ea84819 ci: keep Crabbox hydrate runs reusable 2026-05-22 19:02:52 +01:00
safrano9999
936dfaaac9 Speed up /models browse replies (#84735)
Summary:
- keep default `/models` browse replies on the bounded read-only catalog path
- share the browse catalog loading policy with Gateway model listing
- add helper coverage, preserve full catalog loading for `all` and provider wildcard views, and add the maintainer changelog entry

Verification:
- `node scripts/run-vitest.mjs run --config test/vitest/vitest.agents.config.ts src/agents/model-catalog-browse.test.ts`
- `node scripts/run-vitest.mjs run --config test/vitest/vitest.gateway.config.ts src/gateway/server-methods/models.test.ts`
- `node scripts/run-vitest.mjs run --config test/vitest/vitest.auto-reply-reply.config.ts src/auto-reply/reply/commands-models.test.ts src/auto-reply/reply/directive-handling.model.test.ts`
- `pnpm check:changed` via Blacksmith Testbox `tbx_01ks8bs93c60rjt4ayde91fnjq`
- autoreview clean: no accepted/actionable findings
- GitHub CI, CodeQL, CodeQL Critical Quality, OpenGrep, Workflow Sanity green on `107282aebc2aadde9a3c2acf0cb39fb84b55ade3` before latest changelog-only rebase

Co-authored-by: safrano9999 <240768512+safrano9999@users.noreply.github.com>
2026-05-22 18:56:15 +01:00
Peter Steinberger
9fc5346a97 docs: update changelog for plugin binding command escape (#85188) 2026-05-22 18:54:37 +01:00
Andy Ye
af12082ec8 Let binding commands escape plugin routes 2026-05-22 18:54:37 +01:00
Peter Steinberger
c9b17c5142 ci: fix Crabbox hydrate pnpm modules dir 2026-05-22 18:47:29 +01:00
Pavan Kumar Gondhi
10cb0a5ec0 Restore Control UI gateway token pairing [AI] (#85459)
* fix: restore control ui gateway token pairing

* docs: add changelog entry for PR merge
2026-05-22 23:13:32 +05:30
Peter Steinberger
5e97045345 fix(docker): accept single-object pnpm list output 2026-05-22 18:42:12 +01:00
Peter Steinberger
59aef2ff0d fix: apply docs sweep updates 2026-05-22 18:40:20 +01:00
Vincent Koc
769fd0b14a fix(update): roll back failed git updates 2026-05-23 01:37:39 +08:00
Peter Steinberger
9f1472ed8f test(docker): expect prod store seed command 2026-05-22 18:31:11 +01:00
狼哥
46de078b2a fix(agents): bound embedded compaction write locks
Fixes the embedded attempt session write-lock watchdog so the fallback max hold time follows the resolved compaction timeout plus the existing lock grace window, instead of inheriting the full run timeout.

Adds regression coverage for the helper and settled-compaction lock lifecycle, plus a changelog entry thanking @luoyanglang.

Verification:
- `pnpm test src/agents/session-write-lock.test.ts src/agents/pi-embedded-runner/run/attempt.test.ts src/agents/pi-embedded-runner/run/attempt.session-lock.test.ts`
- `pnpm check:changed` via Blacksmith Testbox `tbx_01ks8b6vn8se5cg1dfn3te3g47` / https://github.com/openclaw/openclaw/actions/runs/26301988670
- Autoreview clean: `/Users/steipete/Projects/agent-scripts/skills/autoreview/scripts/autoreview --mode branch --base origin/main`
- PR CI green on `79e8c5f1a637981d263c0268bf5666967ff4e778`: https://github.com/openclaw/openclaw/actions/runs/26302152844 and https://github.com/openclaw/openclaw/actions/runs/26302152798

Co-authored-by: luoyanglang <hanwanlonga@gmail.com>
2026-05-22 18:30:38 +01:00
Peter Steinberger
de8a82a693 fix(update): repair managed npm plugin peers (#83794) (thanks @fuller-stack-dev) 2026-05-22 18:29:14 +01:00
FullerStackDev
571f364cd7 fix(update): repair managed npm plugin peers 2026-05-22 18:29:14 +01:00
Peter Steinberger
7fc691a426 fix(telegram): honor table mode in outbound chunks (#85455) 2026-05-22 18:26:04 +01:00
Peter Steinberger
d8b973638e fix(docker): precreate owned named volume targets (#85454) 2026-05-22 18:25:19 +01:00
Peter Steinberger
664611c1a5 fix(ui): strip ANSI from displayed gateway logs (#85453)
* fix(ui): strip ANSI from displayed gateway logs

* fix: reuse ansi stripping for ui logs
2026-05-22 18:24:22 +01:00
Peter Steinberger
9210dfc091 fix(skills): accept macos os requirement on darwin (#85451)
* fix(skills): accept macos os requirement on darwin

* fix: satisfy lint for macos os alias
2026-05-22 18:23:31 +01:00
Eva
87b2046575 fix(gateway): preserve message-tool replies in chat history
Preserve current-chat message.send replies in gateway history and live SSE refreshes, while keeping explicit routed sends out of the active chat.

Proof posted on the PR before merge: https://github.com/openclaw/openclaw/pull/84268#issuecomment-4521077098

Co-authored-by: Eva (agent) <eva+agent-78055@100yen.org>
2026-05-22 18:18:18 +01:00
Peter Steinberger
9a816f41a9 test: track Docker prod store seed command 2026-05-22 18:17:36 +01:00
Peter Steinberger
d5247d0bfb fix: satisfy prod store package list lint 2026-05-22 18:13:13 +01:00
Vincent Koc
30333b2e0b test(plugins): clear lookup metadata memo 2026-05-22 19:10:10 +02:00
Peter Steinberger
6788aa1943 fix(docker): seed prod store before offline prune 2026-05-22 18:08:47 +01:00
Peter Steinberger
48bf0374c8 fix(memory): expand home paths in extra memory paths (#85449)
* fix(memory): expand home paths in extra memory paths

* build: refresh shrinkwrap metadata
2026-05-22 18:08:29 +01:00
Peter Steinberger
718cc1b9b6 docs: add security FAQ guidance
Co-authored-by: stevojarvisai-star <stevojarvisai@gmail.com>
2026-05-22 18:05:24 +01:00
Vincent Koc
67c56f34c6 chore(deps): refresh npm shrinkwraps
Refresh root and bundled plugin npm shrinkwraps so the shrinkwrap guard passes on main.
2026-05-23 01:02:18 +08:00
Peter Steinberger
5bb94caef8 docs: clarify OpenAI HTTP client guidance
Refs #52075.
Refs #54275.

Co-authored-by: Francisco <franciscopino1997@gmail.com>
Co-authored-by: 孔祥俊 <xiangjunkong90@gmail.com>
2026-05-22 18:00:53 +01:00
Peter Steinberger
00d3dcaa75 docs: remove stale showcase intro videos 2026-05-22 17:59:14 +01:00
Aman113114-IITD
6ab32bed5c fix(gateway): point model override error to config docs
Summary:
- Point allowModelOverride denial errors to the current configuration reference anchor.

Verification:
- Source check: docs/gateway/configuration-reference.md documents plugins.entries.<id>.subagent.allowModelOverride.
- PR CI: gateway tests and required shards succeeded.
2026-05-22 17:59:10 +01:00
Aman113114-IITD
a003960f26 docs: document secrets provider plan fields
Summary:
- Document providerUpserts and providerDeletes in secrets apply plans.

Verification:
- Source check: src/secrets/plan.ts validates providerUpserts/providerDeletes and src/secrets/apply.ts treats exec provider upserts as exec references.
- PR CI: check-docs succeeded.
2026-05-22 17:59:05 +01:00
Aman113114-IITD
c876fecbe7 docs: clarify media directive formatting
Summary:
- Document that MEDIA directives must be plain-text line-start metadata.

Verification:
- Source check: src/media/parse.ts only recognizes lines whose trimmed start begins with MEDIA: and skips fenced code blocks.
- PR CI: check-docs succeeded.
2026-05-22 17:59:01 +01:00
Aman113114-IITD
884aa1b2eb docs: align memory search cache default
Summary:
- Align memorySearch cache.enabled docs with current runtime default.

Verification:
- Source check: src/agents/memory-search.ts defines DEFAULT_CACHE_ENABLED = true and uses it when cache.enabled is unset.
- PR CI: check-docs succeeded.
2026-05-22 17:58:56 +01:00
Peter Steinberger
c94c513714 refactor(ios): centralize setup auth parsing
Centralizes iOS setup-code auth parsing so token/bootstrap/password trimming and the bootstrap-clears-stale-credentials rule live in one parsed value.

Verification:
- `git diff --check`
- `swiftformat --lint --config config/swiftformat --unexclude apps/ios/Sources apps/ios/Sources/Gateway/GatewayConnectionController.swift apps/ios/Sources/Onboarding/GatewayOnboardingView.swift apps/ios/Sources/Onboarding/OnboardingWizardView.swift apps/ios/Sources/Settings/SettingsTab.swift`
- `swiftlint lint --config apps/ios/.swiftlint.yml apps/ios/Sources/Gateway/GatewayConnectionController.swift apps/ios/Sources/Onboarding/GatewayOnboardingView.swift apps/ios/Sources/Onboarding/OnboardingWizardView.swift apps/ios/Sources/Settings/SettingsTab.swift`
- `AUTOREVIEW_AUTO_TESTS=0 .agents/skills/autoreview/scripts/autoreview --mode branch --base origin/main`
- GitHub merge state clean for `fc35f31e95e73850a153149edaf471c10379dff2`
2026-05-22 17:54:53 +01:00
Peter Steinberger
a0358bbf18 test(release): wait for config reload log proof 2026-05-22 17:44:11 +01:00
Peter Steinberger
d93c59732b refactor(ios): consolidate manual auth override inputs
Consolidates repeated iOS manual-auth override assembly into `ManualAuthOverride.currentManualInput` and reuses the existing `normalized` constructor directly for setup-code pending auth state.

Verification:
- `git diff --check`
- `swiftformat --lint --config config/swiftformat --unexclude apps/ios/Sources apps/ios/Sources/Gateway/GatewayConnectionController.swift apps/ios/Sources/Onboarding/GatewayOnboardingView.swift apps/ios/Sources/Onboarding/OnboardingWizardView.swift apps/ios/Sources/Settings/SettingsTab.swift`
- `swiftlint lint --config apps/ios/.swiftlint.yml apps/ios/Sources/Gateway/GatewayConnectionController.swift apps/ios/Sources/Onboarding/GatewayOnboardingView.swift apps/ios/Sources/Onboarding/OnboardingWizardView.swift apps/ios/Sources/Settings/SettingsTab.swift`
- `AUTOREVIEW_AUTO_TESTS=0 .agents/skills/autoreview/scripts/autoreview --mode branch --base origin/main`
- GitHub merge state clean for `cb6f3bcf8f121b570e22dcb8eed6dc9aaa89fc55`
2026-05-22 17:43:23 +01:00
Peter Steinberger
bb4d88e557 fix(ui): hide thinking options for non-reasoning models (#85406)
* fix(ui): hide thinking options for non-reasoning models

* test(ui): satisfy thinking selector lint

* chore(deps): refresh generated shrinkwraps

* test(ui): remove redundant thinking selector assertion
2026-05-22 17:35:33 +01:00
Peter Steinberger
a03a8d91f6 fix(ui): attach pasted data image text (#85392) 2026-05-22 17:35:14 +01:00
Peter Steinberger
d9c6c5f600 fix(gateway): preserve OpenAI usage aliases in chat history (#85383) 2026-05-22 17:34:24 +01:00
Colin Johnson
e730e9bd0b feat(ios): add realtime talk relay mode
Adds realtime Gateway Talk relay support for iOS, including OpenAI realtime provider selection and voice selection controls.

Maintainer fixups preserved provider auth fallback resolution, kept setup-code/manual auth through TLS trust prompts, recomputed pairing auth from current form fields, fixed the realtime voice label Swift compile issue, added provider auth regression coverage, and refreshed shrinkwrap metadata for the current CI merge base.

Verification:
- `fnm exec --using 24.15.0 pnpm deps:shrinkwrap:check`
- `git diff --check`
- `swiftformat --lint --config config/swiftformat --unexclude apps/ios/Sources apps/ios/Sources/Gateway/GatewayConnectionController.swift apps/ios/Sources/Onboarding/GatewayOnboardingView.swift apps/ios/Sources/Onboarding/OnboardingWizardView.swift apps/ios/Sources/Settings/SettingsTab.swift apps/ios/Sources/Voice/TalkModeGatewayConfig.swift`
- `swiftlint lint --config apps/ios/.swiftlint.yml apps/ios/Sources/Gateway/GatewayConnectionController.swift apps/ios/Sources/Onboarding/GatewayOnboardingView.swift apps/ios/Sources/Onboarding/OnboardingWizardView.swift apps/ios/Sources/Settings/SettingsTab.swift apps/ios/Sources/Voice/TalkModeGatewayConfig.swift`
- `AUTOREVIEW_AUTO_TESTS=0 .agents/skills/autoreview/scripts/autoreview --mode branch --base origin/main`
- GitHub CI clean for `8a76c829611c0eb70d4c3b5328f1868aaf3516e1` (cancelled `auto-response` ignored)

Co-authored-by: Colin Johnson <colin@solvely.net>
2026-05-22 17:34:06 +01:00
Peter Steinberger
933f01cb39 fix(browser): hint WSL portproxy CDP empty replies (#85379)
* fix(browser): hint WSL portproxy CDP empty replies

* build: refresh shrinkwrap metadata after rebase
2026-05-22 17:33:58 +01:00
Vincent Koc
5b90a48e9d fix(installer): persist portable Git on Windows 2026-05-23 00:20:34 +08:00
Peter Steinberger
d22bcfc23a fix(opencode-go): strip Kimi reasoning replay fields (#85377) 2026-05-22 17:13:48 +01:00
Peter Steinberger
81d22c817d fix(build): normalize cache paths on Windows (#85437) 2026-05-22 17:05:44 +01:00
Vincent Koc
adc6adccd8 fix(update): detect nested macOS gateway ancestry (#85391)
* fix(update): detect nested macOS gateway ancestry

* fix(release): refresh shrinkwrap for CI npm

* fix(update): inherit gateway runtime pid for update guard
2026-05-23 00:00:38 +08:00
Peter Steinberger
faf2a6cb9e fix(docker): seed offline prune store in runtime stage 2026-05-22 16:58:12 +01:00
Vincent Koc
21bedd3964 fix(ci): stabilize npm shrinkwrap metadata 2026-05-22 17:50:22 +02:00
Vincent Koc
5cc0dbce86 fix(codex): route node exec through OpenClaw tools
Fixes https://github.com/openclaw/openclaw/issues/85012.\n\nSupersedes https://github.com/openclaw/openclaw/pull/85090 and closes out https://github.com/openclaw/openclaw/pull/83429 as the wrong direction.\n\nVerification before merge:\n- git diff --check origin/main\n- node scripts/run-vitest.mjs extensions/codex/src/app-server/run-attempt.test.ts\n- codex review --base origin/main\n\nNote: the GitHub Real behavior proof check on this maintainer PR was a maintainer bypass, not the live Linux gateway/container plus macOS node proof. User approved merge with this caveat preserved on the PR thread.
2026-05-22 23:43:24 +08:00
Vincent Koc
9364b21e51 test(installer): track portable node root helper 2026-05-22 17:24:59 +02:00
Peter Steinberger
99d7c7077e fix(ui): sync talk transcript translations 2026-05-22 16:19:29 +01:00
Vincent Koc
8fc48af091 fix(ui): localize talk transcript labels
Localize realtime talk transcript aria labels and regenerate Control UI i18n fallback metadata.
2026-05-22 23:12:02 +08:00
Peter Steinberger
cc91ff04cc fix(release): stabilize config restart QA 2026-05-22 15:53:50 +01:00
Peter Steinberger
e842869003 fix(installer): extract portable Node directly 2026-05-22 15:46:28 +01:00
Vincent Koc
dcd98bf1ef test(qa-lab): report scenario pack coverage 2026-05-22 22:35:31 +08:00
Vincent Koc
d70dc4be19 fix(plugins): drop stale tlon tool contract 2026-05-22 16:32:09 +02:00
Peter Steinberger
a54a8813bf fix(installer): prefer tar for portable Node extraction 2026-05-22 15:31:16 +01:00
Bryan P
f9d35dc681 fix(codex): deliver native subagent completions
Deliver Codex-native subagent completions through the generic plugin harness task runtime.

Proof:
- Autoreview clean on final branch.
- Testbox changed gate: tbx_01ks80eqs7d2e3jq3p99zbm4wd, pnpm check:changed, exit 0.
- Live Codex harness: tbx_01ks80p4ky32sqv2ksan2p0w0q, codex/gpt-5.5 API-key auth, native parent/child bridge tokens observed, exit 0.

Co-authored-by: bryanpearson <bryanmpearson@gmail.com>
2026-05-22 15:28:46 +01:00
Josh Lehman
cff5244a5b feat: add context-engine host capability requirements (#84994)
* feat(context-engine): add host capability requirements

* fix(context-engine): advertise pi host capabilities

* fix: repair incompatible context engine slots
2026-05-22 10:28:08 -04:00
Vincent Koc
9d24fde283 fix(release): keep shrinkwrap pinned to pnpm lock 2026-05-22 16:21:52 +02:00
Peter Steinberger
dc04503a7e fix: surface plan updates as status notices 2026-05-22 15:21:19 +01:00
Vincent Koc
fe7d13ca84 test(google): narrow web search fake timers
Narrow Google web search freshness tests to fake Date only.
2026-05-22 22:19:04 +08:00
Peter Steinberger
ffa6cd888f fix(installer): extract portable Node with ZipFile 2026-05-22 15:16:13 +01:00
clawsweeper[bot]
69255f8f32 fix(gateway): defer provider auth prewarm after startup (#85369)
Summary:
- The PR moves gateway provider auth-state prewarm into cancelable post-ready gateway lifetime work, uses current runtime config for delayed warms, and adds related gateway/provider-auth tests plus a changelog entry.
- Reproducibility: no. high-confidence runtime reproduction was run in this review. Source inspection shows th ... th on current main, and the source PR supplies live after-fix proof for the focused startup-ordering slice.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(gateway): defer provider auth prewarm after startup

Validation:
- ClawSweeper review passed for head 31ea4288e3.
- Required merge gates passed before the squash merge.

Prepared head SHA: 31ea4288e3
Review: https://github.com/openclaw/openclaw/pull/85369#issuecomment-4519123491

Co-authored-by: Bob <dutifulbob@gmail.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: osolmaz
Co-authored-by: osolmaz <2453968+osolmaz@users.noreply.github.com>
2026-05-22 14:14:50 +00:00
VACInc
683ad75b31 fix(talk): stabilize realtime voice consults
Stabilize realtime Talk playback, transcript ordering, and consult routing across Android, Web, and the gateway relay.

- serialize Android realtime playback and transcript updates
- add opt-in forced consult routing for Talk realtime sessions
- keep web/gateway consult turns behind OpenClaw results with ordered transcript bubbles
- document the new `talk.realtime.consultRouting` config and keep prompt wording generic

Co-authored-by: VACInc <3279061+VACInc@users.noreply.github.com>
2026-05-22 15:12:39 +01:00
Peter Steinberger
29118a0f0f test(qa): tolerate slow gateway rpc startup 2026-05-22 15:10:38 +01:00
Vincent Koc
ab684f5088 chore(diagnostics): refresh plugin sdk baseline 2026-05-22 22:01:41 +08:00
Vincent Koc
513195b462 fix(diagnostics): surface async queue drops 2026-05-22 22:01:41 +08:00
Vincent Koc
bdcaac06c6 fix(diagnostics): bound diagnostic buffers 2026-05-22 22:01:41 +08:00
Peter Steinberger
c21ca883b0 fix(installer): copy portable Node into place 2026-05-22 14:51:26 +01:00
Mason Huang
6ea907cec1 fix(cli): recover replaced device approvals (#85342)
Summary:
- The PR teaches `openclaw devices approve <requestId>` to approve a compatible same-device replacement request during local fallback and adds focused CLI, infra, and changelog coverage.
- Reproducibility: yes. Source inspection shows current main rejects the gateway's replacement requestId as a  ...  adds focused infra and CLI tests for the churn path; I did not run tests because this review is read-only.

Automerge notes:
- PR branch already contained follow-up commit before automerge: docs: note device approval recovery

Validation:
- ClawSweeper review passed for head 1d2f2e9b2f.
- Required merge gates passed before the squash merge.

Prepared head SHA: 1d2f2e9b2f
Review: https://github.com/openclaw/openclaw/pull/85342#issuecomment-4518449317

Co-authored-by: masonxhuang <masonxhuang@tencent.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: hxy91819
Co-authored-by: hxy91819 <8814856+hxy91819@users.noreply.github.com>
2026-05-22 13:44:15 +00:00
Peter Steinberger
0def3e20e4 test(release): align prerelease validation 2026-05-22 14:43:36 +01:00
Peter Steinberger
2890b1a24a fix(installer): install portable Node directory atomically 2026-05-22 14:36:50 +01:00
zhang-guiping
937a756f7f fix(runtime-llm): avoid duplicate provider prefix in allowlist diagnostics (#84946)
normalizeAllowedModelRef() and the resolved override ref interpolated
${provider}/${model} after normalizeModelRef(), so a provider-qualified
model id like openrouter/gpt-5.4-mini surfaced as
openrouter/openrouter/gpt-5.4-mini in the allowlist set and policy
denial message, masking the actionable model ref.

Route both sites through modelKey() (src/agents/model-ref-shared.ts)
so the provider segment is collapsed when the model id already starts
with it. Add regression tests covering allowlist hit and denial paths
for the OpenRouter shape.

Fixes #84887
2026-05-22 21:36:13 +08:00
Jayesh Betala
66d1d13889 fix(gateway): include openclaw bin in service PATH (#84475)
* fix(gateway): include openclaw bin in service PATH

* fix(doctor): accept expected service PATH

* docs(changelog): mention managed service PATH bin fix

---------

Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-05-22 21:28:50 +08:00
googlerest
ba86716999 fix(gateway): handle concurrent launchd bootstrap restart race (#84722)
* Handle concurrent launchd bootstrap restart

* docs(changelog): mention launchd bootstrap restart race fix

---------

Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-05-22 21:21:23 +08:00
Peter Steinberger
31a189db0a feat: support pi and opencode autoreview engines 2026-05-22 14:20:54 +01:00
Vincent Koc
52759294ca ci(package): gate acceptance on package integrity 2026-05-22 21:17:20 +08:00
Peter Steinberger
fea89cd384 ci(release): bypass pnpm for tsdown package build 2026-05-22 14:16:37 +01:00
Peter Steinberger
04ebdc6da5 test(release): align prerelease validation baselines 2026-05-22 14:15:46 +01:00
Peter Steinberger
7b1fbe1c37 ci(release): harden docker package build 2026-05-22 14:15:46 +01:00
Peter Steinberger
c3531fcd7b fix(codex): skip native web search transcript mirroring (#85346)
* fix(codex): skip native web search transcript mirroring

* test(codex): type transcript snapshot assertion
2026-05-22 14:14:01 +01:00
NianJiu
fc7a531f6c fix(gateway): harden launchd reload handoff race recovery (#84641)
* fix(gateway): harden launchd reload handoff race recovery

* docs(changelog): mention launchd reload handoff race fix

---------

Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-05-22 21:13:26 +08:00
Huvee
ca2b9ad289 fix: honor per-model provider transport overrides (#80488)
Summary:
- Honor per-model api/baseUrl overrides during custom provider auth hook lookup and transport selection.
- Keep models-add metadata safeguards intact and add focused auth/model resolver regression coverage.
- Add maintainer changelog credit for @huveewomg.

Verification:
- git diff --check
- GitHub CI green on 277629e992
- GitHub CodeQL green on 277629e992
- GitHub CodeQL Critical Quality green on 277629e992
- GitHub Real behavior proof green on 277629e992
- Local focused Vitest was stopped after 8 minutes on a busy host without producing a result; PR CI supplied the final proof.

Co-authored-by: huveewomg <wongrenthou1265@gmail.com>
2026-05-22 14:12:11 +01:00
openperf
19ff77e9c9 fix(skills): document watcher edge cases, add teardown/rebuild tests, add changelog 2026-05-22 14:10:56 +01:00
openperf
bb73f0a5c3 fix(skills): type watcher mock calls in dedupe regression tests 2026-05-22 14:10:56 +01:00
openperf
3e94290460 fix(skills): dedupe shared-directory watchers across agent workspaces (#84968) 2026-05-22 14:10:56 +01:00
Peter Steinberger
47d66fe343 fix(infra): allow macos browser open over ssh env (#85340) 2026-05-22 14:07:19 +01:00
Vincent Koc
a15797ad11 fix(update): preserve package service state during cutover (#83026)
* fix(update): preserve package service state during cutover

* docs(changelog): mention package service state cutover fix
2026-05-22 21:02:58 +08:00
Vincent Koc
07e61fc847 fix(gateway): broadcast agent-run error payloads (#85355) 2026-05-22 20:58:36 +08:00
Vincent Koc
a28f1297ab test(e2e): avoid synthetic channel config in plugin smoke 2026-05-22 14:55:10 +02:00
Peter Steinberger
a00c58363a fix(cli): suppress systemd hints for live gateway (#85336)
* fix(cli): suppress systemd hints for live gateway

* test(cli): type systemd hint mock
2026-05-22 13:52:21 +01:00
Peter Steinberger
fc47c1f55e fix(cli): honor agent for model auth logout (#85326) 2026-05-22 13:46:07 +01:00
Tung, Hsiao-Yu
4a9138556e fix(gateway): eager-load lifecycle runtime to survive in-place upgrades (#84890)
* fix(gateway): eager-load lifecycle runtime to survive in-place upgrades

After a package-swap update (e.g. via update.run), dist/ chunk hashes
rotate while the gateway is still running. The SIGUSR1 listener's first
dynamic import of the lifecycle runtime module then throws
ERR_MODULE_NOT_FOUND inside its async IIFE, silently rejects, and leaves
restart.ts's emittedRestartToken permanently unconsumed. From that point
every scheduleGatewaySigusr1Restart() — including the one update.run
schedules for itself — returns { coalesced: true } without scheduling
anything, and the gateway never restarts until manually kickstarted.

Fix:

1. Eagerly resolve the lifecycle runtime module as the first statement
   of runGatewayLoop, before any signal listener is installed. lifecycle.runtime
   is a 36-line re-export hub, so loading it once pulls the entire restart
   / respawn / queue / sentinel / handoff graph into memory, immune to
   later disk rotation. If the module is missing at startup, fail fast
   with a loud error so the supervisor can recover instead of running
   half-broken.

2. Defense in depth: catch SIGUSR1 IIFE rejections and call
   markGatewaySigusr1RestartHandled() via the eagerly captured reference,
   so a transient listener failure doesn't permanently stick the restart
   token.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* docs(changelog): mention lifecycle restart eager load

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-05-22 20:44:05 +08:00
Peter Steinberger
111bad1065 fix(doctor): point codex asset warning at migrate plan (#85324) 2026-05-22 13:40:15 +01:00
Jason O'Neal
1bafc23ae3 fix(update): harden managed handoff cwd (#83875)
* fix(update): harden managed handoff cwd

* docs(changelog): mention managed update handoff cwd fix

---------

Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-05-22 20:35:17 +08:00
Peter Steinberger
e43282e701 docs(release): prepare 2026.5.21 notes 2026-05-22 13:30:19 +01:00
Peter Steinberger
f4bdfd46a9 ci(crabbox): harden docker hydration 2026-05-22 13:28:53 +01:00
Mason Huang
57db041365 refactor(crabbox): parse provider list from binary help instead of hardcoding (#85302)
Summary:
- The branch replaces the Crabbox wrapper's hardcoded provider allow-list with help-output parsing, preserves current aliases and a known help omission, adds wrapper tests, and updates the changelog.
- Reproducibility: yes. source-reproducible: current main only rejects selected providers that are already in  ... rovider names can bypass wrapper validation. I did not run the PR branch because this review was read-only.

Automerge notes:
- PR branch already contained follow-up commit before automerge: refactor(crabbox): parse provider list from binary help instead of ha…
- PR branch already contained follow-up commit before automerge: fix(clawsweeper): address review for automerge-openclaw-openclaw-8530…

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

Prepared head SHA: c99388d92a
Review: https://github.com/openclaw/openclaw/pull/85302#issuecomment-4517730136

Co-authored-by: masonxhuang <masonxhuang@tencent.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: hxy91819
Co-authored-by: hxy91819 <8814856+hxy91819@users.noreply.github.com>
2026-05-22 12:25:15 +00:00
Vincent Koc
84329182a7 test(plugins): keep rpc source walk on source call gateway 2026-05-22 14:13:21 +02:00
Vincent Koc
01e7f64629 test(plugins): run kitchen sink rpc lane without tsx 2026-05-22 14:13:21 +02:00
Vincent Koc
6f6da5f5ba test(plugins): add kitchen sink rpc docker lane 2026-05-22 14:13:21 +02:00
Vincent Koc
2b396131e4 test(qa-lab): add bus tool trace scenario 2026-05-22 20:12:49 +08:00
Peter Steinberger
ebfb834dcd fix(cron): classify network retry errors (#85344) 2026-05-22 13:07:40 +01:00
Vincent Koc
3551e98433 fix(installer): bootstrap portable Windows Node 2026-05-22 19:59:45 +08:00
Alex Knight
1fdc73ae4b fix(ui): move chat session search into picker (#85303)
* fix(ui): move chat session search into picker

* fix(ui): smooth chat picker search controls

* test(ui): add mocked chat picker e2e harness
2026-05-22 21:57:00 +10:00
Peter Steinberger
84af5e6e76 fix: honor shrinkwrap when bundling plugin deps 2026-05-22 12:56:10 +01:00
Peter Steinberger
9914e25638 fix: opt acpx out of bundled runtime deps 2026-05-22 12:56:10 +01:00
Peter Steinberger
8b0537c409 test: refresh shrinkwrap after rebase 2026-05-22 12:56:10 +01:00
Peter Steinberger
fcecbd8655 fix: opt codex out of bundled runtime deps 2026-05-22 12:56:10 +01:00
Peter Steinberger
249f79be42 fix: make bundled plugin packages portable 2026-05-22 12:56:10 +01:00
Peter Steinberger
86faf654db fix: keep bundled plugin peers nested 2026-05-22 12:56:10 +01:00
Peter Steinberger
976da39038 fix: publish explicit plugin bundled dependencies 2026-05-22 12:56:10 +01:00
Peter Steinberger
3784270670 chore: refresh shrinkwrap metadata 2026-05-22 12:56:10 +01:00
Peter Steinberger
de022bb69d feat: bundle plugin npm dependencies 2026-05-22 12:56:10 +01:00
Vincent Koc
0d28040092 fix: honor overrides in npm shrinkwrap generation 2026-05-22 12:56:10 +01:00
Vincent Koc
bfa5b39648 fix: cover plugin package locks in dependency review 2026-05-22 12:56:10 +01:00
Vincent Koc
a1b05aae7c test: update shrinkwrap packaging expectations 2026-05-22 12:56:10 +01:00
Vincent Koc
82f69a269b fix: include plugin shrinkwraps in dependency reports 2026-05-22 12:56:10 +01:00
Vincent Koc
b2dc4492f0 chore: refresh shrinkwrap for Testbox npm 2026-05-22 12:56:10 +01:00
Peter Steinberger
b6c8807ca0 chore: add shrinkwrap to plugin npm packages 2026-05-22 12:56:10 +01:00
Peter Steinberger
c56067e34f chore: harden npm shrinkwrap release path 2026-05-22 12:56:10 +01:00
Eva
56308a7144 fix: limit subagent bootstrap defaults
Limit sub-agent bootstrap context to AGENTS.md and TOOLS.md without adding a new config surface. Preserve the existing cron minimal bootstrap behavior.

Co-authored-by: Eva (agent) <eva+agent-78055@100yen.org>
2026-05-22 12:55:42 +01:00
Peter Steinberger
ab1fedb63f feat: update autoreview engine coverage 2026-05-22 12:38:15 +01:00
Peter Steinberger
89c59a89fb fix(agents): preserve OpenAI transport error metadata 2026-05-22 12:38:06 +01:00
Neerav Makwana
0a95e53602 fix(messages): strip unsupported citation markers (#85204) (thanks @neeravmakwana)
Co-authored-by: Neerav Makwana <261249544+neeravmakwana@users.noreply.github.com>
2026-05-22 12:33:03 +01:00
Vincent Koc
fda0baf98d test(qa-lab): report live transport coverage lanes 2026-05-22 19:31:32 +08:00
Chunyue Wang
136c927140 fix(gateway): close child ACP sessions on parent reset/delete
Close child ACP runtimes during parent reset/delete through a shared direct-child session lookup, covering spawnedBy and parentSessionKey lineage across combined agent stores.

Also adds focused regression coverage for direct child discovery, non-ACP/unrelated negatives, reset cleanup, delete cleanup, cross-store children, and concurrent stuck-child cleanup.

Co-authored-by: openperf <16864032@qq.com>
2026-05-22 12:29:24 +01:00
clawsweeper[bot]
77a1b7625d fix: preserve Google Gemini 3 cron thinking (#85300)
Summary:
- The branch adds a Google provider thinking-policy resolver and opt-in profile flag, updates shared thinking validation and cron/proof-policy tests, and adjusts ClawSweeper proof parsing.
- Reproducibility: yes. source-reproducible: current main applies the generic off-only profile before provider ... figured thinking through that resolver. I did not execute a live systemd cron run in this read-only review.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix: preserve Google Gemini 3 cron thinking

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

Prepared head SHA: a6cd2e826e
Review: https://github.com/openclaw/openclaw/pull/85300#issuecomment-4517662575

Co-authored-by: Neerav Makwana <261249544+neeravmakwana@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
2026-05-22 11:21:57 +00:00
Peter Steinberger
85e468d275 docs(skills): exclude SDK boundary bug sweeps 2026-05-22 12:17:02 +01:00
Peter Steinberger
c9a0f03dd7 feat(plugin-sdk): add generic channel poll sender (#85299)
* feat(plugin-sdk): add generic channel poll sender

* test(channels): follow durable capability list

* test(channels): update poll capability expectations

* fix(channels): normalize poll receipt parts
2026-05-22 12:16:07 +01:00
Peter Steinberger
0ddf51cf71 fix(agents): preserve OpenAI reasoning token usage 2026-05-22 12:14:12 +01:00
Vincent Koc
1a8625529e test(e2e): harden plugin smoke cleanup 2026-05-22 13:06:03 +02:00
Peter Steinberger
6b1c8687b5 fix(plugins): resolve native plugin sdk aliases (#85298)
* fix(plugins): resolve native plugin sdk aliases

* fix(plugins): satisfy native resolver lint

* fix(plugins): keep native sdk aliases on js artifacts
2026-05-22 12:05:37 +01:00
Vincent Koc
03f61cd1b5 fix(update): keep service logs out of json output 2026-05-22 12:46:00 +02:00
Vincent Koc
ff79299d68 fix(agent): retry transient gateway handshake closes 2026-05-22 12:24:50 +02:00
Krzysztof Probola
8523e0930e fix(codex): keep interrupted turns visible-answer eligible (#84494)
* fix(codex): keep interrupted turns visible-answer eligible

* docs(changelog): note codex interrupted recovery

---------

Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-05-22 18:22:20 +08:00
Vincent Koc
6bd430ee35 test(agents): narrow bundle mcp e2e setup 2026-05-22 11:39:24 +02:00
Alex Knight
e2f82d4d30 test: add mocked Control UI E2E tests and playwright for local verification and development (#85278)
* test: add control ui mocked e2e
2026-05-22 19:36:38 +10:00
Peter Steinberger
70dd31506b fix: land code-mode structured worker errors (#83444) (thanks @Kaspre) 2026-05-22 10:36:18 +01:00
Kaspre
edab653178 fix(code-mode): return structured worker error codes 2026-05-22 10:36:18 +01:00
Peter Steinberger
0d8c9ca914 fix: preserve route-bound direct thread events 2026-05-22 10:32:12 +01:00
Peter Steinberger
0acfb7ba13 fix: route direct thread event wakes to main DMs 2026-05-22 10:32:12 +01:00
Peter Steinberger
4ee8a2ac2e fix: break plugin metadata snapshot cycle 2026-05-22 10:32:12 +01:00
Peter Steinberger
7b489560f3 test: align exec event routing proof (#83743) (thanks @Kaspre) 2026-05-22 10:32:12 +01:00
Kaspre
37207c6925 test node exec event wake metadata 2026-05-22 10:32:12 +01:00
Kaspre
e53612a639 fix heartbeat event routing for main-scoped DMs 2026-05-22 10:32:12 +01:00
Vincent Koc
d24cfcfa21 test(plugins): retry bundled smoke health probes 2026-05-22 17:21:48 +08:00
Vincent Koc
2b1c01f769 test(gateway): bind auth-free websocket harness to loopback 2026-05-22 11:17:22 +02:00
Vincent Koc
a12e3022db test(plugins): keep npm peer prune mock directory-safe 2026-05-22 11:12:17 +02:00
github-actions[bot]
40b8dd88d8 chore(ui): refresh fa control ui locale 2026-05-22 09:11:58 +00:00
Vincent Koc
b859654641 fix(ci): allow release update restarts 2026-05-22 11:11:53 +02:00
github-actions[bot]
cc6d222ae3 chore(ui): refresh nl control ui locale 2026-05-22 09:11:46 +00:00
github-actions[bot]
b59ab5b1f0 chore(ui): refresh vi control ui locale 2026-05-22 09:11:42 +00:00
github-actions[bot]
f483f59b6c chore(ui): refresh th control ui locale 2026-05-22 09:11:27 +00:00
github-actions[bot]
c222ef01e9 chore(ui): refresh id control ui locale 2026-05-22 09:11:08 +00:00
github-actions[bot]
0050b8e89a chore(ui): refresh pl control ui locale 2026-05-22 09:11:05 +00:00
github-actions[bot]
6b4aec9fb9 chore(ui): refresh uk control ui locale 2026-05-22 09:11:02 +00:00
github-actions[bot]
940a950e47 chore(ui): refresh tr control ui locale 2026-05-22 09:10:27 +00:00
github-actions[bot]
d11c2e421d chore(ui): refresh ar control ui locale 2026-05-22 09:10:22 +00:00
github-actions[bot]
c99a29d0a8 chore(ui): refresh it control ui locale 2026-05-22 09:10:19 +00:00
github-actions[bot]
a7ba47c4ee chore(ui): refresh fr control ui locale 2026-05-22 09:10:13 +00:00
github-actions[bot]
a5fa944c69 chore(ui): refresh ko control ui locale 2026-05-22 09:09:46 +00:00
github-actions[bot]
f3a984dcbb chore(ui): refresh ja-JP control ui locale 2026-05-22 09:09:43 +00:00
github-actions[bot]
4711bb529a chore(ui): refresh es control ui locale 2026-05-22 09:09:40 +00:00
github-actions[bot]
4d6b3845f1 chore(ui): refresh zh-TW control ui locale 2026-05-22 09:09:30 +00:00
github-actions[bot]
d6fc2f34a3 chore(ui): refresh pt-BR control ui locale 2026-05-22 09:09:11 +00:00
github-actions[bot]
3222e35322 chore(ui): refresh zh-CN control ui locale 2026-05-22 09:09:05 +00:00
github-actions[bot]
ea5b5d78d5 chore(ui): refresh de control ui locale 2026-05-22 09:09:01 +00:00
Alex Knight
5d01be1070 Add chat picker search and pagination 2026-05-22 19:07:21 +10:00
Peter Steinberger
b3ec11b052 docs: add changelog for swept bugfix PRs 2026-05-22 10:00:35 +01:00
Vincent Koc
bf64de9191 fix(plugins): keep derived metadata snapshots fresh
Keep derived plugin metadata snapshots out of the process memo/current snapshot cache so newly added plugins under derived discovery paths are found without restart.
2026-05-22 17:00:09 +08:00
Vincent Koc
beccdde5bf fix(qa): isolate patched suite scenarios 2026-05-22 10:59:23 +02:00
吴杨帆
a80476fbe9 fix(telegram): preserve fenced code languages (#85209)
Co-authored-by: wuyangfan <yangfan.wu@succaiss.com>
2026-05-22 09:59:06 +01:00
Julyan
6f933656e5 fix: strip -plugin suffix in deriveIdHint to match manifest ids (#85170)
The deriveIdHint function already strips -provider from unscoped
package names (@openclaw/anthropic-provider -> anthropic) but does
not strip -plugin (@openclaw/xai-plugin -> xai-plugin instead of
xai). This causes ~30 spurious 'plugin id mismatch' warnings on
gateway startup for built-in plugins whose package names end in
-plugin.

Closes #85048
2026-05-22 09:58:56 +01:00
Sergio Cadavid
1b0a5d1627 fix(openai): preserve codex gpt-5.5 image input (#85095) 2026-05-22 09:58:43 +01:00
Noah
fb61de8c88 fix(gemini): strip sub-second precision from web_search time_range_filter (#85071)
* fix(gemini): strip sub-second precision from web_search time_range_filter

Gemini's google_search.time_range_filter rejects any non-zero fractional
seconds with "[FIELD_INVALID] Granularity of nano is not supported", even
though the underlying google.protobuf.Timestamp type accepts 0/3/6/9
fractional digits per its public spec. The grounding endpoint enforces a
stricter rule than the underlying type.

Date.prototype.toISOString() always emits millisecond precision, so every
freshness call (and any date_after/date_before call hitting the "now"
fallback for endTime) failed with the above 400 after #66498's fix shipped
in 2026.5.19.

Introduce toGeminiTimeRangeTimestamp() which strips the fractional-second
component before serializing, and route all four timeRangeFilter timestamp
sites through it. isoDateExclusiveEnd happens to produce all-zero
fractional today (so Gemini accepts it), but routing it through the helper
keeps the contract uniform and resilient to future changes.

Why this slipped past the original CI: the existing freshness test used
vi.setSystemTime(new Date("2026-04-15T12:00:00Z")), which always
serializes back as ".000Z" — the one fractional form Gemini happens to
accept. Wall-clock new Date() in production always has non-zero ms. The
new test uses setSystemTime(new Date("2026-04-15T12:00:00.123Z")) to
exercise the realistic case.

Verified empirically against the live Gemini REST API:
  ".123Z" → 400 "Granularity of nano is not supported"
  ".000Z" → grounded content (the one fractional form accepted)
  "Z"     → grounded content

Fixes #85061.

* test(gemini): use realistic non-zero ms in existing freshness test

The original test set the fake clock to a moment with zero fractional
seconds, so toISOString() produced ".000Z" — the one fractional form
Gemini's google_search.time_range_filter happens to accept. Wall-clock
new Date() in production produces non-zero ms, which Gemini rejects.

Bumping the fake time to .123Z makes the existing test exercise the
realistic case alongside the dedicated regression test.

---------

Co-authored-by: Noah R <Noerr@users.noreply.github.com>
2026-05-22 09:58:29 +01:00
Vincent Koc
9bd97d2c60 test(qa-lab): remove generic evidence wording 2026-05-22 16:54:04 +08:00
Vincent Koc
a9176e9190 fix(ci): reject embedded fallback release turns 2026-05-22 10:51:55 +02:00
Peter Steinberger
88ad5cb2f4 feat: update autoreview skill 2026-05-22 09:47:22 +01:00
Peter Steinberger
25e489395a docs: add changelog for code mode hook params (#83483) (thanks @Kaspre) 2026-05-22 09:46:27 +01:00
Kaspre
1e1e45b72b fix(code-mode): align outer exec hook params 2026-05-22 09:46:27 +01:00
Pavan Kumar Gondhi
ea5f2abb48 fix(integrations): enforce channel read target allowlists [AI] (#84982)
* fix: enforce message read target allowlists

* addressing review-skill

* addressing review-skill

* addressing review-skill

* addressing review-skill

* addressing codex review

* addressing codex review

* addressing codex review

* addressing ci

* addressing ci

* docs: add changelog entry for PR merge
2026-05-22 14:11:06 +05:30
Peter Steinberger
23961fe472 fix(codex): bound app-server client-close retries
Co-authored-by: VACInc <3279061+VACInc@users.noreply.github.com>
2026-05-22 09:37:35 +01:00
Vincent Koc
0a4b30191d fix(ci): time packaged fresh release phases 2026-05-22 10:24:17 +02:00
Jason (Json)
37a9f58d1b Fix media completion duplicate delivery (#84006)
Summary:
- The PR changes generated-media duplicate guards, completion delivery fallback behavior, transcript write-lock reuse, task-registry fresh owner reads, docs, changelog, and regression coverage.
- Reproducibility: yes. with source and artifact evidence rather than a local rerun: current main completes me ... e task and one successful video task after the patch. I did not run tests because this review is read-only.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix: dedupe media completion delivery
- PR branch already contained follow-up commit before automerge: fix: avoid music provider lookup for explicit models
- PR branch already contained follow-up commit before automerge: fix: narrow detached media task handles
- PR branch already contained follow-up commit before automerge: fix: close media completion review gaps
- PR branch already contained follow-up commit before automerge: fix: tolerate media delivery mirrors during session lock
- PR branch already contained follow-up commit before automerge: Fix media completion duplicate delivery

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

Prepared head SHA: f83e3bf143
Review: https://github.com/openclaw/openclaw/pull/84006#issuecomment-4484835103

Co-authored-by: fuller-stack-dev <263060202+fuller-stack-dev@users.noreply.github.com>
Co-authored-by: FullerStackDev <263060202+fuller-stack-dev@users.noreply.github.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
2026-05-22 08:18:16 +00:00
Peter Steinberger
5ed8bbc694 fix(gateway): preserve stop reason for deferred agent aborts 2026-05-22 09:16:36 +01:00
Peter Steinberger
9ff3b9f4ef docs: update changelog for agent SIGTERM fix (#84381) 2026-05-22 09:16:36 +01:00
Kaspre
fd293bd2a7 test(gateway): use real dedupe map in abort helpers 2026-05-22 09:16:36 +01:00
Kaspre
01fce88082 fix(agent): abort accepted gateway runs on signal 2026-05-22 09:16:36 +01:00
Peter Steinberger
192a782b99 chore: add landable bug sweep skill 2026-05-22 09:08:48 +01:00
Vincent Koc
6981051682 fix(testing): ignore Crabbox metadata in changed lanes 2026-05-22 09:50:41 +02:00
Peter Steinberger
e201fbfbd2 fix: preserve xAI web search credential fallback (#85182) 2026-05-22 08:49:53 +01:00
fuller-stack-dev
ec8ed79646 fix: report oauth web search in onboarding summary 2026-05-22 08:49:53 +01:00
fuller-stack-dev
3b21a3f4c4 fix: pass active agent to web search execution 2026-05-22 08:49:53 +01:00
fuller-stack-dev
27088c6919 fix: use active agent auth for xai web search 2026-05-22 08:49:53 +01:00
fuller-stack-dev
65471a2da6 feat: add xai oauth web search and provider timeouts 2026-05-22 08:49:53 +01:00
samzong
014b527e23 fix: stop denied exec followups (#85194)
Stops denied exec approvals from feeding agent follow-up work, suppresses node `exec.denied` wakeups, adds Chinese stop phrases to abort handling, and documents terminal denial behavior.

Fixes #69386.

Co-authored-by: samzong <samzong.lu@gmail.com>
2026-05-22 08:48:19 +01:00
Peter Steinberger
b2a0bfab43 refactor(gateway): split connect assembly 2026-05-22 08:46:18 +01:00
samzong
0e47815e6e fix(gateway): surface connect assembly failures
Surface local post-challenge connect assembly failures immediately instead of waiting for the Gateway CLI wrapper timeout.\n\nCo-authored-by: samzong <samzong.lu@gmail.com>
2026-05-22 08:40:06 +01:00
Sarah Fortune
49e3f8c3ee fix(models) Discord model picker doesn't list all models (#85138)
* Add pagination to the discord model picker

* Ensure current model is shown as selected in the picker when its first loaded
2026-05-21 23:50:13 -07:00
WhatsSkiLL
170f72d5a1 fix(models): resolve set aliases from runtime config [AI-assisted] (#83262)
Summary:
- The branch passes runtime config into the model config write helper, updates `openclaw models set` to resolve aliases source-first then runtime-fallback, and adds regression tests plus a changelog entry.
- Reproducibility: yes. I did not execute the CLI in this read-only review, but the current-main source path a ... ing against source config while runtime defaults can be the only place the displayed `sonnet` alias exists.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(models): preserve authored aliases for set
- PR branch already contained follow-up commit before automerge: fix(models): resolve set aliases from runtime config [AI-assisted]

Validation:
- ClawSweeper review passed for head 29138ac5d0.
- Required merge gates passed before the squash merge.

Prepared head SHA: 29138ac5d0
Review: https://github.com/openclaw/openclaw/pull/83262#issuecomment-4472495568

Co-authored-by: JARVIS-Glasses <284122573+JARVIS-Glasses@users.noreply.github.com>
Co-authored-by: IWhatsskill <284122573+IWhatsskill@users.noreply.github.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
2026-05-22 06:31:44 +00:00
clawsweeper[bot]
17e2ccf179 fix(exec): return approved WebChat gateway exec output inline (#85239)
Summary:
- The PR changes gateway exec approval handling so native WebChat approvals wait for the decision and return a ... al as the exec tool result, while preserving async follow-ups for diagnostics-direct and non-WebChat paths.
- Reproducibility: yes. Current-main source and tests show approval-required gateway exec returns approval-pen ... linked source PR provides live WebChat canary output showing the fixed inline result after native approval.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(exec): return approved WebChat gateway exec output inline

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

Prepared head SHA: 7182322015
Review: https://github.com/openclaw/openclaw/pull/85239#issuecomment-4515339946

Co-authored-by: Zac-W <wangzhifengzac@gmail.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
2026-05-22 06:30:34 +00:00
Alex Knight
d0a74dbfbe fix codex memory flush tool surface (#85220)
Co-authored-by: Alex Knight <15041791+amknight@users.noreply.github.com>
2026-05-22 16:23:25 +10:00
Sarah Fortune
b01a078d83 revert(models): drop auth-profiles.json fs watcher (#85244)
The watcher fired constantly because the gateway itself rewrites
auth-profiles.json frequently (cooldown ticks, usage stats, OAuth
refresh, markAuthProfileFailure). Each self-write triggered chokidar
which cleared the prepared auth map and scheduled an ~8 s rewarm,
a feedback loop that defeated the caching the rest of the PR added.

Drop the watcher entirely. Self-heal still covers the stale-TRUE
direction via the markAuthProfileFailure hook. Stale-FALSE (user adds
auth externally and the gateway hasn't observed any request through
that profile yet) reverts to the pre-PR behavior: reload config or
restart gateway to pick it up. Known limitation.
2026-05-21 23:11:50 -07:00
Andy Ye
03125c8e13 Validate Codex app-server command overrides (#84417)
Summary:
- The PR rejects Codex app-server command overrides that embed Node/package-manager inline arguments, adds matching doctor diagnostics, regression tests, and a changelog entry.
- Reproducibility: yes. for the scoped malformed override path: current main passes the combined command strin ... ix resolver/doctor live output. I did not establish a live Windows npm-global managed-startup reproduction.

Automerge notes:
- PR branch already contained follow-up commit before automerge: Validate Codex app-server command overrides

Validation:
- ClawSweeper review passed for head 966bcd6617.
- Required merge gates passed before the squash merge.

Prepared head SHA: 966bcd6617
Review: https://github.com/openclaw/openclaw/pull/84417#issuecomment-4494295224

Co-authored-by: Andy Ye <35905412+TurboTheTurtle@users.noreply.github.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
2026-05-22 05:39:02 +00:00
Sarah Fortune
62a330e752 perf(models): make provider auth checks non-blocking
Make provider-auth checks asynchronous so catalog and model-listing sweeps yield between slow auth discovery calls.
2026-05-21 22:37:32 -07:00
Alex Knight
cc4e30b3d9 fix(codex): extend message tool timeout (#85216)
* fix(codex): extend message tool timeout

---------

Co-authored-by: Alex Knight <15041791+amknight@users.noreply.github.com>
2026-05-22 15:36:25 +10:00
Kaspre
e32e0f3f7f fix(channels): pass allowBootstrap from channel-selection so in-agent message tool resolves channels in --local processes (#85022)
Summary:
- The branch passes `allowBootstrap: true` through outbound channel selection, preserves bundled-plugin resolution before bootstrap, adds focused regression tests, and documents the fix in the changelog.
- Reproducibility: yes. source inspection gives a high-confidence reproduction path: current main omits `allow ... run the live current-main failure, but the supplied after-fix terminal proof exercises the implicated path.

Automerge notes:
- PR branch already contained follow-up commit before automerge: test(channels): cover bootstrap channel selection
- PR branch already contained follow-up commit before automerge: fix(channels): avoid unnecessary bootstrap during message sends
- PR branch already contained follow-up commit before automerge: fix(channels): pass allowBootstrap from channel-selection so in-agent…

Validation:
- ClawSweeper review passed for head 44099a80e8.
- Required merge gates passed before the squash merge.

Prepared head SHA: 44099a80e8
Review: https://github.com/openclaw/openclaw/pull/85022#issuecomment-4510333662

Co-authored-by: Kaspre <kaspre@gmail.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
2026-05-22 05:20:15 +00:00
Alex Knight
6a3377255d [codex] add color mode tooltips (#85227)
* fix(ui): add color mode tooltips

* docs: update changelog for color mode tooltips

* docs: credit changelog contributor

---------

Co-authored-by: Alex Knight <15041791+amknight@users.noreply.github.com>
2026-05-22 15:15:20 +10:00
Alex Knight
8df350030d fix(ui): show all configured chat picker sessions
Remove the chat picker recency/current-agent filters while preserving the bounded configured-agent refresh, and add the changelog credit for @amknight.
2026-05-22 15:14:55 +10:00
Alex Knight
b7356e4e58 fix(ui): clarify inherited thinking off label (#85223)
Control UI thinking selectors now show inherited disabled reasoning as Inherited: Off while keeping explicit Off distinct.
2026-05-22 15:14:30 +10:00
Josh Avant
b010852dc6 fix(telegram): dedupe replayed message dispatches (#85208)
* Fix Telegram dispatch replay dedupe

* Add changelog for Telegram dispatch dedupe

* Persist Telegram replay dedupe at dispatch start
2026-05-21 22:14:16 -07:00
Jason (Json)
cd1cae5be9 fix(auto-reply): preserve sessions after compaction failures (#70479)
Summary:
- The PR removes the auto-reply compaction-failure session reset hook, adds preserved-session recovery guidance for overflow/compaction failure paths, and updates focused tests, docs, and the changelog.
- Reproducibility: yes. at source level with high confidence. Current main routes both embedded overflow paylo ... resetSessionAfterCompactionFailure, and the PR body includes before/after terminal proof of those branches.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(auto-reply): drop dead compaction reset hook
- PR branch already contained follow-up commit before automerge: fix(auto-reply): preserve sessions after compaction failures

Validation:
- ClawSweeper review passed for head 193d3c0fdd.
- Required merge gates passed before the squash merge.

Prepared head SHA: 193d3c0fdd
Review: https://github.com/openclaw/openclaw/pull/70479#issuecomment-4325128777

Co-authored-by: FullerStackDev <263060202+fuller-stack-dev@users.noreply.github.com>
Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
2026-05-22 05:04:41 +00:00
Galin Iliev
93c613cec4 fix: drop unsafe Copilot reasoning replay IDs 2026-05-22 05:02:27 +00:00
Sarah Fortune
55cfe00a3a fix(models): handle watcher errors, close on shutdown, rewarm after invalidate
Addresses three ClawSweeper findings on the fs-watcher commit:

- [P1] auth-profile watcher now handles chokidar 'error' events (logs +
  closes once) mirroring the gateway config-reload pattern. Without
  this, an unhandled error from chokidar can crash the gateway.

- [P2] auth-profile watcher handle is pushed into postReadySidecars so
  stopPostReadySidecarsAfterCloseStarted closes it on gateway shutdown.

- [P2] auth-failure and file-change invalidation paths now schedule a
  background rewarm (with a 'reason=' log line). Without this, the next
  /models call after an invalidation paid the slow per-provider path
  until the next reload. The warmer's existing generation counter
  handles concurrent rewarms safely.
2026-05-21 21:52:21 -07:00
Sarah Fortune
06a6d2b5c9 fix(models): watch auth-profiles.json so externally added creds become visible
Adds a chokidar watcher on every configured agent's auth-profiles.json.
Any change fires clearCurrentProviderAuthState so the next model-listing
call recomputes against the on-disk auth state. Closes the stale-FALSE
direction (user adds auth via codex login, hand-edit, etc.) that the
auth-failure hook can't catch on its own.
2026-05-21 21:52:21 -07:00
Sarah Fortune
a1bdffc212 test(auth-profiles): cover self-heal hook firing + survives hook errors 2026-05-21 21:52:21 -07:00
Sarah Fortune
ab265dbce9 fix(models): log auth-profile failure hook errors instead of swallowing them 2026-05-21 21:52:21 -07:00
Sarah Fortune
a483f70a8a fix(models): self-heal prepared auth on auth-profile failure
When markAuthProfileFailure observes an auth failure at request time
(token rotated, OAuth revoke, etc.), fire a hook that clears the
prepared provider-auth map so the next model-listing call recomputes
against the real auth state. Single mutable hook slot wired up at
gateway startup; no TTL or polling.

Addresses ClawSweeper's P1 freshness finding on #85125 without
reintroducing the TTL.
2026-05-21 21:52:21 -07:00
Sarah Fortune
95343affbb Remove ttl on auth config. Prewarm prepared config for each agent. Key by agent ID instead of agent dir 2026-05-21 21:52:21 -07:00
Kevin Lin
1008b8213b fix(slack): keep approvals in app conversation threads
* fix(slack): keep plugin approvals in app conversation threads

* fix(slack): preserve plugin approval routing

* fix(slack): keep suppression typing aligned

* fix(slack): suppress native dm approval fallback

* fix(slack): suppress stored native approval fallback
2026-05-21 21:51:34 -07:00
Pavan Kumar Gondhi
229490a489 fix: constrain Windows task script names [AI] (#85064)
* fix: validate windows task script file names

* addressing ci

* docs: add changelog entry for PR merge
2026-05-22 09:46:52 +05:30
Kaspre
5f0bec4479 fix(agent): await local agent_end hooks (#85007)
Summary:
- The PR adds an awaited `agent_end` helper, uses it for no-channel local CLI and Codex app-server terminal pa ... erves fire-and-forget behavior for channel-backed paths, and updates docs, changelog, and regression tests.
- Reproducibility: yes. by source inspection. Current main calls `runAgentHarnessAgentEndHook` without awaiting in local CLI and Codex terminal paths, and the PR's pending-hook tests encode the failure and desired split.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(agents): await local agent_end hooks
- PR branch already contained follow-up commit before automerge: test: fix agent_end hook helper fixture
- PR branch already contained follow-up commit before automerge: ci: retry security checkout
- PR branch already contained follow-up commit before automerge: ci: allowlist qa-lab lifecycle fixtures
- PR branch already contained follow-up commit before automerge: fix CLI channel agent_end delivery
- PR branch already contained follow-up commit before automerge: ci: drop stale qa-lab deadcode entries

Validation:
- ClawSweeper review passed for head 97b31379d7.
- Required merge gates passed before the squash merge.

Prepared head SHA: 97b31379d7
Review: https://github.com/openclaw/openclaw/pull/85007#issuecomment-4509911851

Co-authored-by: Kaspre <kaspre@gmail.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
2026-05-22 04:12:22 +00:00
WhatsSkiLL
7dc2e50ac3 fix(channels): bypass debounce for bare abort triggers [AI-assisted] (#83348)
Summary:
- The PR changes shared, Feishu, Mattermost, Microsoft Teams, and WhatsApp inbound debounce predicates so bare abort text bypasses debounce, then adds focused tests and a changelog entry.
- Reproducibility: yes. source-level. Current main sends bare `stop`, `abort`, and `wait` through a `hasContro ... ()` debounce gate, while the existing abort-aware detector and trigger set already recognize those phrases.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(channels): bypass debounce for bare abort triggers [AI-assisted]
- PR branch already contained follow-up commit before automerge: fix(clawsweeper): address review for automerge-openclaw-openclaw-8334…

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

Prepared head SHA: c96bf84270
Review: https://github.com/openclaw/openclaw/pull/83348#issuecomment-4473176095

Co-authored-by: IWhatsskill <284122573+IWhatsskill@users.noreply.github.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
2026-05-22 04:09:33 +00:00
2326 changed files with 164578 additions and 19924 deletions

View File

@@ -1,16 +1,17 @@
---
name: autoreview
description: "Autoreview closeout: local dirty changes, PR branch vs main, parallel tests."
description: "Auto Review closeout. Codex review is the default when no engine is set and is the recommended reviewer."
---
# Autoreview
# Auto Review
Run Codex's built-in code review as a closeout check. This is code review (`codex review`), not Guardian `auto_review` approval routing.
Run the bundled structured review helper as a closeout check. This is code review, not Guardian `auto_review` approval routing.
Codex native review mode performs best and is recommended. Non-Codex reviewers are fallback/second-opinion paths that receive a generated diff prompt, not the full Codex review-mode runtime.
Codex review is the default when no engine is set. It usually delivers the best review results and should remain the normal final closeout engine.
Use when:
- user asks for Codex review / autoreview / second-model review
- user asks for Codex review / Claude review / autoreview / second-model review
- after non-trivial code edits, before final/commit/ship
- reviewing a local branch or PR branch after fixes
@@ -21,60 +22,64 @@ Use when:
- Read dependency docs/source/types when the finding depends on external behavior.
- Reject unrealistic edge cases, speculative risks, broad rewrites, and fixes that over-complicate the codebase.
- Prefer small fixes at the right ownership boundary; no refactor unless it clearly improves the bug class.
- Keep going until the selected review path returns no accepted/actionable findings.
- If a review-triggered fix changes code, rerun focused tests and rerun the review helper.
- Default to Codex review with no fallback. Prefer Codex for final closeout because it uses native review mode; non-Codex reviewers use a Codex-inspired generated diff prompt. Use `--fallback-reviewer auto|claude|pi|opencode|droid|copilot` only when a second-model fallback is explicitly wanted and authenticated. The helper runs nested Codex review in yolo/full-access mode by default; use `--no-yolo` only when intentionally testing sandbox behavior.
- Stop as soon as the review command/helper exits 0 with no accepted/actionable findings. Do not run an extra direct `codex review` just to get a nicer "clean" line, a second opinion, or clearer closeout wording.
- Keep going until structured review returns no accepted/actionable findings.
- If a review-triggered fix changes code, rerun focused tests and rerun the structured review helper.
- For security-audit suppression changes, verify accepted findings remain auditable: suppressed findings stay in structured output, active output keeps an unsuppressible suppression notice, and aggregate findings cannot hide unrelated active risk.
- Never switch or override the requested review engine/model. If the review hits model capacity, retry the same command a few times with the same engine/model.
- Tools are useful in review mode. The helper allows read-only inspection tools and web search by default so reviewers can check dependency contracts, upstream docs, and current behavior.
- Security perspective is always included, but it should not cripple legitimate functionality. Report security findings only when the change creates a concrete, actionable risk or removes an important safety check.
- Do not invoke built-in `codex review`, nested reviewers, or reviewer panels from inside the review. The helper builds one bundle, calls one selected engine, validates one structured result, and stops.
- Stop as soon as the helper exits 0 with no accepted/actionable findings. Do not run an extra review just to get a nicer "clean" line, a second opinion, or clearer closeout wording.
- Treat the helper's successful exit plus absence of actionable findings as the clean review result, even if the underlying Codex CLI output is terse.
- Multi-reviewer panels are opt-in only. Use them when explicitly requested or when risk justifies the extra spend; the main agent still verifies every accepted finding before fixing.
- If rejecting a finding as intentional/not worth fixing, add a brief inline code comment only when it explains a real invariant or ownership decision that future reviewers should know.
- If creating or updating a PR while rejecting any autoreview finding, record the rejected finding and reason in the PR description so later reviewers can distinguish intentional design decisions from missed review output.
- If `gh`/Gitcrawl reports `database disk image is malformed`, run `gitcrawl doctor --json` once to let the portable cache repair before retrying review; do not bypass the shim unless repair fails and freshness requires live GitHub.
- If Gitcrawl reports a portable manifest mismatch, source/runtime DB health error, or stale portable-store checkout, run `gitcrawl doctor --json` and inspect `source_db_health`, `runtime_db_health`, and `portable_store_status` before falling back to live GitHub.
- Do not push just to review. Push only when the user requested push/ship/PR update.
- For OpenClaw maintainers, keep autoreview validation Crabbox/Testbox-aware when maintainer validation mode is enabled (`OPENCLAW_TESTBOX=1` or `AUTOREVIEW_OPENCLAW_MAINTAINER_VALIDATION=1`). A review pass may inspect files and run cheap non-Node probes, but it must not start local `pnpm`, Vitest, `tsgo`, `npm test`, or `node scripts/run-vitest.mjs` from a Codex/worktree review unless the operator explicitly requested local proof. For runtime proof, use existing evidence or route through Crabbox/Testbox and report the id. Do not apply this rule to ordinary contributors who do not have maintainer Testbox access.
## Pick Target
Dirty local work:
```bash
codex review --uncommitted
<autoreview-helper> --mode local
```
Use this only when the patch is actually unstaged/staged/untracked in the
current checkout. For committed, pushed, or PR work, point Codex at the commit
current checkout. For committed, pushed, or PR work, point the helper at the commit
or branch diff instead; do not force `--mode local` / `--uncommitted` just
because the helper docs mention dirty work first. A clean `--uncommitted` review
because the helper docs mention dirty work first. A clean local review
only proves there is no local patch.
Branch/PR work:
```bash
git fetch origin
codex review --base origin/main
<autoreview-helper> --mode branch --base origin/main
```
Do not pass any prompt with `--base`, `--commit`, or `--uncommitted`. Codex CLI
review targets and custom review prompts are mutually exclusive: target modes
generate their own review prompt internally. Use plain target review for native
Codex closeout, or use custom prompt review (`codex review -`) only when you
intentionally want a generated diff prompt instead of native target review.
Optional review context is first-class:
```bash
<autoreview-helper> --mode branch --base origin/main --prompt-file /tmp/review-notes.md --dataset /tmp/evidence.json
```
If an open PR exists, use its actual base:
```bash
base=$(gh pr view --json baseRefName --jq .baseRefName)
codex review --base "origin/$base"
<autoreview-helper> --mode branch --base "origin/$base"
```
Committed single change:
```bash
codex review --commit HEAD
<autoreview-helper> --mode commit --commit HEAD
```
or with the helper:
```bash
.agents/skills/autoreview/scripts/autoreview --mode commit --commit HEAD
/Users/steipete/Projects/agent-scripts/skills/autoreview/scripts/autoreview --mode commit --commit HEAD
```
Use commit review for already-landed or already-pushed work on `main`. Reviewing
@@ -87,60 +92,93 @@ with `--base`.
Format first if formatting can change line locations. Then it is OK to run tests and review in parallel:
```bash
.agents/skills/autoreview/scripts/autoreview --parallel-tests "<focused test command>"
scripts/autoreview --parallel-tests "<focused test command>"
```
Tradeoff: tests may force code changes that stale the review. If tests or review lead to code edits, rerun the affected tests and rerun review until no accepted/actionable findings remain. Once that rerun exits cleanly, stop; do not spend another long review cycle on redundant confirmation.
## Review Panels
Run multiple reviewers against one frozen bundle:
```bash
<autoreview-helper> --reviewers codex,claude
```
`--panel` is shorthand for Codex plus Claude unless `--engine` changes the first reviewer:
```bash
<autoreview-helper> --panel
```
Set reviewer models and thinking/effort explicitly:
```bash
<autoreview-helper> --reviewers codex,claude --model codex=gpt-5.1 --thinking codex=high --model claude=sonnet --thinking claude=max
```
Inline syntax is also supported:
```bash
<autoreview-helper> --reviewers codex:gpt-5.1:high,claude:sonnet:max
```
Codex maps thinking to `model_reasoning_effort` and accepts `low`, `medium`,
`high`, or `xhigh`. Claude maps thinking to `--effort` and also accepts `max`.
Engines without a real thinking knob reject `--thinking`.
## Context Efficiency
Codex review is usually noisy. Default to a subagent filter when subagents are available. Ask it to run the review and return only:
- actionable findings it accepts
- findings it rejects, with one-line reason
- exact files/tests to rerun
Run inline only for tiny changes or when subagents are unavailable.
Run the helper directly so target selection, engine choice, structured validation, and exit status all stay in one path. If output is noisy, summarize the completed helper output after it returns; do not ask another agent or reviewer to rerun the review.
## Helper
Bundled helper:
OpenClaw repo-local helper:
```bash
.agents/skills/autoreview/scripts/autoreview --help
```
`agent-scripts` checkout helper:
```bash
skills/autoreview/scripts/autoreview --help
```
Global helper from `agent-scripts`:
```bash
~/.codex/skills/agent-scripts/autoreview/scripts/autoreview --help
```
If installed from `agent-scripts`, path is:
```bash
/Users/steipete/Projects/agent-scripts/skills/autoreview/scripts/autoreview --help
```
The helper:
- chooses dirty `--uncommitted` first
- chooses dirty local changes first
- otherwise uses current PR base if `gh pr view` works
- otherwise uses `origin/main` for non-main branches
- auto-runs `PNPM_CONFIG_PM_ON_FAIL=ignore PNPM_CONFIG_VERIFY_DEPS_BEFORE_RUN=false PNPM_CONFIG_OFFLINE=true pnpm run check` in parallel when a repo has `package.json`, `pnpm-lock.yaml`, `node_modules`, and a `check` script; disable with `AUTOREVIEW_AUTO_TESTS=0`
- supports `--engine codex`, `claude`, `droid`, and `copilot`; default is `AUTOREVIEW_ENGINE` or `codex`; Codex should remain the default when nothing is set
- use `--mode commit --commit <ref>` for already-committed work, especially clean `main` after landing
- should be left in `--mode auto` or forced to `--mode branch` for PR/branch work; do not force `--mode local` after committing
- supports `--reviewer codex|claude|pi|opencode|droid|copilot|auto`; `auto` means Codex first
- supports `--fallback-reviewer auto|claude|pi|opencode|droid|copilot|none`; default is `none`
- falls back only when Codex is unavailable or exits nonzero, not when Codex reports findings
- writes only to stdout unless `--output` or `AUTOREVIEW_OUTPUT` is set
- supports `--dry-run`, `--parallel-tests`, and commit refs
- runs nested review with `--dangerously-bypass-approvals-and-sandbox --sandbox danger-full-access` by default
- with `OPENCLAW_TESTBOX=1` or `AUTOREVIEW_OPENCLAW_MAINTAINER_VALIDATION=1`, disables auto local `pnpm run check` and routes Codex through generated prompt review (`codex review -`) so the no-local-heavy-tests policy is included; native Codex target review cannot accept extra prompt text
- non-Codex reviewers receive the generated diff prompt and maintainer validation policy text when maintainer validation is active
- keeps accepting `--full-access`; use `--no-yolo` or `AUTOREVIEW_YOLO=0` to opt out
- still accepts legacy `CODEX_REVIEW_*` env vars when the matching `AUTOREVIEW_*` var is unset
- writes only to stdout unless `--output` or `--json-output` is set
- supports `--dry-run`, `--parallel-tests`, `--prompt`, `--prompt-file`, `--dataset`, `--no-tools`, `--no-web-search`, and commit refs
- supports opt-in review panels with `--panel` / `--reviewers`, plus per-engine `--model` and `--thinking`
- allows read-only tools and web search by default where the selected CLI supports them; forbids nested review in the prompt; Codex is run through `codex exec` with read-only sandbox and structured output
- prints `autoreview clean: no accepted/actionable findings reported` when the selected review command exits 0
- exits nonzero when accepted/actionable findings are present
## Final Report
Include:
- review command used
- tests/proof run
- findings accepted/rejected, briefly why
- the clean review result from the final helper/review run, or why a remaining finding was consciously rejected
Do not run another Codex review solely to improve the final report wording. If the final helper run exited 0 and produced no accepted/actionable findings, report that exact run as clean.
## PR / CI Closeout
- Prefer direct run/job APIs after CI starts: `gh run view <run-id> --json jobs`; use PR rollup only for final mergeability.
- After rebase, compare `origin/main..HEAD`; drop CI-fix commits already upstream before pushing.
- For prompt snapshot CI failures, prove/generate with Linux Node 24 before rerunning the failed job.
- Update PR body once near the final head unless proof labels are missing or stale enough to block CI.
Do not run another review solely to improve the final report wording. If the final helper run exited 0 and produced no accepted/actionable findings, report that exact run as clean.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,176 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'EOF'
Usage: test-review-harness [--fixture malicious|benign] [--engine codex|claude|droid|copilot]...
Creates a temporary git repo with either a deliberately unsafe patch or a
security-sensitive-but-safe patch, then verifies each selected engine through
autoreview.
Default engines: codex, claude.
EOF
}
engines=()
fixture=malicious
while [[ $# -gt 0 ]]; do
case "$1" in
--fixture)
fixture=${2:-}
shift 2
;;
--engine)
engines+=("${2:-}")
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
usage >&2
exit 2
;;
esac
done
case "$fixture" in
malicious|benign) ;;
*)
usage >&2
exit 2
;;
esac
if [[ ${#engines[@]} -eq 0 ]]; then
engines=(codex claude)
fi
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
repo=$(mktemp -d "${TMPDIR:-/tmp}/autoreview-fixture.XXXXXX")
trap 'rm -rf "$repo"' EXIT
cd "$repo"
git init --quiet
git config user.name "Review Fixture"
git config user.email "review-fixture@example.com"
if [[ "$fixture" == malicious ]]; then
cat > app.js <<'EOF'
export function uploadPath(name) {
return `uploads/${name.replaceAll("/", "")}`;
}
export function publicUser(user) {
return { id: user.id, name: user.name };
}
EOF
else
cat > app.js <<'EOF'
import path from "node:path";
const uploadsRoot = path.resolve("uploads");
function safeChildPath(root, name) {
if (name.includes("/") || name.includes("\\") || name.includes("\0")) {
throw new Error("invalid child name");
}
const resolved = path.resolve(root, name);
if (!resolved.startsWith(root + path.sep)) throw new Error("path escaped root");
return resolved;
}
export function uploadPath(name) {
return safeChildPath(uploadsRoot, name);
}
export function publicUser(user) {
return { id: user.id, name: user.name };
}
EOF
fi
git add app.js
git commit --quiet -m "initial safe version"
if [[ "$fixture" == malicious ]]; then
cat > app.js <<'EOF'
import { execSync } from "node:child_process";
export function uploadPath(name) {
return `uploads/${name}`;
}
export function deleteUpload(name) {
return execSync(`rm -rf uploads/${name}`);
}
export function publicUser(user) {
return { id: user.id, name: user.name, password: user.password };
}
EOF
else
cat > app.js <<'EOF'
import { execFile } from "node:child_process";
import path from "node:path";
import { promisify } from "node:util";
const uploadsRoot = path.resolve("uploads");
const reposRoot = path.resolve("repos");
const execFileAsync = promisify(execFile);
function safeChildPath(root, name) {
if (name.includes("/") || name.includes("\\") || name.includes("\0")) {
throw new Error("invalid child name");
}
const resolved = path.resolve(root, name);
if (!resolved.startsWith(root + path.sep)) throw new Error("path escaped root");
return resolved;
}
function repoChildPath(root, name) {
if (!/^[A-Za-z0-9._-]+$/.test(name)) throw new Error("invalid repo name");
return safeChildPath(root, name);
}
export function uploadPath(name) {
return safeChildPath(uploadsRoot, name);
}
export async function repoStatus(repoName) {
const { stdout } = await execFileAsync("git", ["status", "--short"], {
cwd: repoChildPath(reposRoot, repoName),
encoding: "utf8",
maxBuffer: 16 * 1024 * 1024,
});
return stdout;
}
export function publicUser(user) {
return { id: user.id, name: user.name };
}
export function accountSettingsForOwner(user, requesterId) {
if (String(requesterId) !== String(user.id)) throw new Error("forbidden");
return { id: user.id, hasPassword: Boolean(user.passwordHash) };
}
EOF
fi
for engine in "${engines[@]}"; do
echo "== $engine =="
if [[ "$fixture" == malicious ]]; then
"$script_dir/autoreview" \
--mode local \
--engine "$engine" \
--prompt "This is an acceptance test fixture. The changed app.js patch contains real security bugs. Review normally and report only actionable defects from the patch." \
--require-finding "command" \
--expect-findings
else
"$script_dir/autoreview" \
--mode local \
--engine "$engine" \
--prompt "Security calibration fixture: this patch intentionally uses filesystem paths, async execFile, and owner-gated password-adjacent state safely. Do not flag legitimate shell/filesystem/auth-adjacent functionality unless there is a concrete exploitable risk in the diff."
fi
done

View File

@@ -0,0 +1,74 @@
---
name: control-ui-e2e
description: Use when testing, fixing, or extending the OpenClaw Control UI GUI with Vitest + Playwright end-to-end checks, mocked Gateway WebSocket flows, mocked dashboard runs, screenshots/videos, or agent-verifiable browser proof.
---
# Control UI E2E
Use this for Control UI changes that need a real browser flow with deterministic Gateway data.
## Test Shape
- Use `ui/src/**/*.e2e.test.ts` for full GUI flows.
- Use `ui/src/test-helpers/control-ui-e2e.ts` to start the Vite Control UI and install a mocked Gateway WebSocket.
- Keep scenarios deterministic. Do not use live provider keys, real channel credentials, or a real Gateway unless the user explicitly asks for live proof.
- Prefer existing `.browser.test.ts` or unit tests for narrow rendering logic; use this E2E lane when the proof should cover routing, app boot, Gateway handshake, requests, and visible UI behavior together.
## Commands
- Target one E2E test in a Codex worktree:
```bash
node scripts/run-vitest.mjs run --config test/vitest/vitest.ui-e2e.config.ts --configLoader runner ui/src/ui/e2e/chat-flow.e2e.test.ts
```
- Run the whole local lane in a normal checkout:
```bash
pnpm test:ui:e2e
```
If dependencies are missing in a Codex worktree, install once with `pnpm install`; for broad GUI proof or dependency-heavy checks, use Testbox/Crabbox instead of running a wide local pnpm lane.
## Visual Proof Default
When running mocked Control UI/dashboard validation for a user-facing feature, produce visual proof by default unless the user explicitly opts out.
- Keep the Vitest E2E assertions deterministic; do not commit generated screenshots or videos.
- After or alongside the focused E2E test, run the mocked Control UI app when available, for example `pnpm dev:ui:mock -- --port <port>`.
- Drive Chromium with Playwright against the local mock URL and capture a video plus screenshots for each meaningful state: initial view, interaction input, result state, and final/paginated/selected state.
- Use `browser.newContext({ recordVideo: { dir, size }, viewport })`, `page.screenshot({ path })`, and close the context before reporting the video path.
- Put artifacts under `.artifacts/control-ui-e2e/<short-feature-name>/` or another clearly named local temp directory, and report the absolute paths in the final answer.
- Treat recording as validation, not only demo capture. If the recorder fails or shows surprising behavior, stop, fix the behavior, add or update a regression test, then rerecord.
- If visual proof is blocked, state the exact blocker and still report the textual E2E evidence.
## Mock Pattern
Start the app server, install the mock before `page.goto`, then assert both Gateway traffic and visible UI:
```ts
const server = await startControlUiE2eServer();
const page = await context.newPage();
const gateway = await installMockGateway(page, {
historyMessages: [{ role: "assistant", content: [{ type: "text", text: "Ready." }] }],
});
await page.goto(`${server.baseUrl}chat`);
await page.locator(".agent-chat__composer-combobox textarea").fill("hello");
await page.getByRole("button", { name: "Send message" }).click();
const request = await gateway.waitForRequest("chat.send");
await gateway.emitChatFinal({ runId: String(request.params.idempotencyKey), text: "Done." });
await page.getByText("Done.").waitFor();
```
Extend `installMockGateway` with typed scenario options or method responses when a new flow needs more Gateway surface.
## Standalone Recording
When recording an already-running mocked Control UI URL, use a temporary Playwright script or `playwright test` spec and keep the recording flow focused:
- Open the mock URL, interact through stable `data-*` selectors or user-facing role selectors, and wait on asserted states instead of relying on fixed sleeps.
- Assert both visible UI state and mocked Gateway traffic for request-driven flows. For example, verify the expected count/row is visible and that `sessions.list` was called with the expected `search`, `offset`, and `limit`.
- Use short sleeps only after assertions to make the captured video readable.
- Store the generated video under `.artifacts/control-ui-e2e/<feature>/`; do not commit it.

View File

@@ -0,0 +1,4 @@
interface:
display_name: "Control UI E2E"
short_description: "Mocked browser E2E for Control UI"
default_prompt: "Use $control-ui-e2e to verify a Control UI change with the mocked Vitest + Playwright browser lane."

View File

@@ -0,0 +1,165 @@
---
name: openclaw-landable-bug-sweep
description: "Find or repair small high-confidence non-SDK-boundary OpenClaw bugfix PRs until five are landable."
---
# OpenClaw Landable Bug Sweep
Autonomous maintainer workflow for producing five landable OpenClaw bugfix PR URLs.
Use for broad issue/PR sweeps where the bar is high and the output is PRs, not notes.
Do not use for plugin SDK/API boundary work; those need separate architecture review.
## Target
Return exactly five PR URLs, each with:
- bug summary
- why the fix is low-risk
- proof: rebased-head local/Testbox/live commands or run IDs
- autoreview: clean result on the exact head being shown
- CI green on the exact pushed PR head
- issue/duplicate cleanup done or still pending
The five URLs may be existing PRs that were reviewed/fixed, or new PRs created from issues/clusters.
Do not present a PR URL to the maintainer until it has been refreshed on current `main`, left-tested, autoreviewed clean, pushed, and verified green in live GitHub CI.
If code, tests, changelog, PR body, or branch base changes after autoreview, rerun autoreview before showing the URL.
## Companion Skills
Use `$gitcrawl` for discovery/clustering, `$openclaw-pr-maintainer` for live GitHub mutation rules, `$github-author-context` when contributor trust matters, `$openclaw-testing` for proof choice, `$autoreview` before publishing/landing, and `$crabbox` for broad/E2E/live proof.
## Candidate Bar
Accept only when all are true:
- bug or paper cut, not feature/product/support/docs-only
- root cause is proven in current code
- dependency behavior checked via upstream docs/source/types when relevant
- production/runtime diff is small, ideally much smaller than 500 LOC and always below 500 LOC
- tests may be larger, but focused
- no new dependency
- no new config option
- no backward-incompatible behavior
- no security/product/owner-boundary decision needed
- no plugin SDK, public plugin API, or `src/plugin-sdk/**` boundary change
- no broad refactor smell
- focused proof is feasible
- branch can be rebased/refreshed and pushed, or a replacement PR can be created
Good examples:
- provider parameter mismatch proven against dependency/API contract
- CLI command diverges from adjacent command behavior
- narrow runtime state/serialization bug with failing test
- issue already fixed on current `main`, with proof and closeable duplicates
Reject:
- feature requests, new knobs, migrations, release work, workflow policy, support
- plugin SDK/API boundary changes, including compatibility shims, new SDK methods, SDK exports, or plugin-facing channel/provider seams
- auth/security boundary changes unless explicitly assigned
- bugs needing live credentials that are unavailable
- PRs with red CI unless you fix, rebase, push, and recheck them green
- PRs you only reviewed locally but did not refresh/push/check live
- PRs whose final head has not passed `$autoreview`
- fixes whose clean shape is a larger architecture move
- speculative reports without reproducible/provable cause
- UI/UX changes requiring product judgment
## Sweep Loop
1. Start clean:
- `git status -sb`
- `git pull --ff-only`
- verify branch is expected, usually `main`
2. Build candidate clusters:
- `gitcrawl` open issues/PRs, neighbors, and search
- live `gh issue/pr view`
- include PRs linked from issues and duplicates
3. For each cluster:
- read issue/PR body, comments, labels, linked refs, current source, adjacent tests
- suppress maintainer-owned queue noise unless it is the best fix path
- identify opener/author and preserve credit
- decide: `repair-existing-pr`, `create-new-pr`, `close-fixed-on-main`, `close-duplicate`, or `reject`
4. Prove before patching:
- failing test, focused repro, log/source proof, or dependency contract proof
- if already fixed on `main`, prove with current source/test/commit and close kindly
5. Patch:
- prefer existing PR when good and writable
- if unwritable or wrong shape, create own PR and preserve useful contributor credit
- if no PR exists, create one
- add regression test when it fits
- changelog for user-facing fixes; thank credited human reporter/contributor
6. Review, refresh, and publish:
- rebase or otherwise refresh the PR branch on current `origin/main`
- resolve drift, including newly exposed CI failures, rather than counting the PR as ready
- changelog-only conflicts are routine on busy `main`; resolve them mechanically when already refreshing, but do not treat them as a real code conflict, a reason to reject the PR, or evidence that the branch needs extra fixup beyond the changelog entry order
- left-test the rebased head with the smallest meaningful local/Testbox/live command that proves the bug
- run `$autoreview` until no accepted/actionable findings remain before creating, updating, or presenting the PR URL
- create/update PR with real body and proof fields
- push the exact reviewed head
- verify live GitHub CI is green for that pushed head; do not count pending, red, dirty, conflicting, or externally blocked PRs in the five
7. Hygiene:
- close duplicates and fixed-on-main issues/PRs with proof as soon as you notice them during the sweep
- never mutate more than five associated items in one cluster without explicit confirmation
- comments must be kind, concrete, and include proof/PR/commit links
8. Repeat until five landable PR URLs are ready.
## PR Body Proof
Use the repo PR template. Include these exact labels:
```text
Behavior addressed:
Real environment tested:
Exact steps or command run after this patch:
Evidence after fix:
Observed result after fix:
What was not tested:
```
## Existing PR Rules
- Review code path beyond the diff before trusting it.
- If PR is good: rebase/refresh on current `main`, fix small issues, left-test, autoreview clean, push, and get CI green before showing or counting it.
- If PR is not good but has a useful idea: recreate locally, co-author when warranted, close original with thanks and explanation.
- If PR is duplicate or fixed on `main`: comment proof, close.
- If maintainer cannot push to contributor branch: create own branch/PR, preserve useful commits or credit.
- If CI turns red after local proof, treat that as normal work: inspect the failing job, fix or reject, rerun, and only count the PR once green.
## Output Ledger
Maintain a running ledger:
```text
accepted:
- PR URL:
source refs:
bug:
root cause:
fix:
risk:
rebase/head:
left-test:
autoreview:
CI:
credit/thanks:
cleanup:
rejected:
- ref:
reason:
closed:
- ref:
reason:
proof/comment:
```
Final answer:
- exactly five accepted PR URLs
- 2-4 sentence explainer per PR
- proof/CI state per PR
- closed duplicates/fixed-on-main refs
- current branch/status

View File

@@ -0,0 +1,4 @@
interface:
display_name: "OpenClaw Landable Bug Sweep"
short_description: "Find five small non-SDK landable bugfix PRs"
default_prompt: "Use $openclaw-landable-bug-sweep to find or repair five small high-confidence non-SDK-boundary OpenClaw bugfix PRs and get them landable."

View File

@@ -58,7 +58,7 @@ Use this skill for Parallels guest workflows and smoke interpretation. Do not lo
- For beta/stable verification, resolve the tag immediately before the run (`npm view openclaw@beta version dist.tarball` or `npm view openclaw@latest ...`). Tags can move while a long VM matrix is already running; restart the matrix when the intended prerelease appears after an earlier registry 404/tag-lag check.
- Use the configured secret workflow to inject only the provider keys needed by OpenAI/Anthropic lanes. Do not print secrets or env dumps; pass provider secrets through the guest exec environment.
- Same-guest update verification should set the default model explicitly to `openai/gpt-5.4` before the agent turn and use a fresh explicit `--session-id` so old session model state does not leak into the check.
- The aggregate npm-update wrapper must resolve the Linux VM with the same Ubuntu fallback policy as `parallels-linux-smoke.sh` before both fresh and update lanes. Treat any Ubuntu guest with major version `>= 24` as acceptable when the exact default VM is missing, preferring the closest version match. On Peter's current host today, missing `Ubuntu 24.04.3 ARM64` should fall back to `Ubuntu 25.10`.
- The aggregate npm-update wrapper must resolve the Linux VM with the same Ubuntu fallback policy as `parallels-linux-smoke.sh` before both fresh and update lanes. Treat any Ubuntu guest with major version `>= 24` as acceptable when the exact default VM is missing, preferring the newest versioned Ubuntu guest with a fresh poweroff snapshot. On Peter's current host today, use `Ubuntu 26.04`.
- On macOS same-guest update checks, restart the gateway after the npm upgrade before `gateway status` / `agent`; launchd can otherwise report a loaded service while the old process has exited and the fresh process is not RPC-ready yet.
- The npm-update aggregate's macOS update leg writes the guest update script as root, then runs it as the desktop user. If `prlctl exec "$MACOS_VM" --current-user ...` cannot authenticate, retry through plain root `prlctl exec` plus `sudo -u <desktop-user> /usr/bin/env HOME=/Users/<desktop-user> USER=<desktop-user> LOGNAME=<desktop-user> PATH=/opt/homebrew/bin:/opt/homebrew/opt/node/bin:/usr/bin:/bin:/usr/sbin:/sbin ...`. That is a Parallels transport fallback; still verify `openclaw --version`, gateway RPC, and an agent turn after the update.
- On Windows same-guest update checks, restart the gateway after the npm upgrade before `gateway status` / `agent`; in-place global npm updates can otherwise leave stale hashed `dist/*` module imports alive in the running service.
@@ -93,8 +93,8 @@ Use this skill for Parallels guest workflows and smoke interpretation. Do not lo
- If that release-to-dev lane fails with `reason=preflight-no-good-commit` and repeated `sh: pnpm: command not found` tails from `preflight build`, treat it as an updater regression first. The fix belongs in the git/dev updater bootstrap path, not in Parallels retry logic.
- Until the public stable train includes that updater bootstrap fix, the macOS release-to-dev lane may seed a temporary guest-local `pnpm` shim immediately before `openclaw update --channel dev`. Keep that workaround scoped to the smoke harness and remove it once the latest stable no longer needs it.
- In Tahoe `prlctl exec --current-user` runs, prefer explicit `node .../openclaw.mjs ...` invocations for the release->dev handoff itself and for post-update verification. The shebanged global `openclaw` wrapper can fail with `env: node: No such file or directory`, and self-updating through the wrapper is a weaker lane than invoking the entrypoint under a fixed `node`.
- Default to the snapshot closest to `macOS 26.3.1 latest`.
- On Peter's Tahoe VM, `fresh-latest-march-2026` can hang in `prlctl snapshot-switch`; if restore times out there, rerun with `--snapshot-hint 'macOS 26.3.1 latest'` before blaming auth or the harness.
- Default to the snapshot closest to `macOS 26.5 latest`.
- On Peter's Tahoe VM, `fresh-latest-march-2026` can hang in `prlctl snapshot-switch`; if restore times out there, rerun with `--snapshot-hint 'macOS 26.5 latest'` before blaming auth or the harness.
- `parallels-macos-smoke.sh` now retries `snapshot-switch` once after force-stopping a stuck running/suspended guest. If Tahoe still times out after that recovery path, then treat it as a real Parallels/host issue and rerun manually.
- The macOS smoke should include a dashboard load phase after gateway health: resolve the tokenized URL with `openclaw dashboard --no-open`, verify the served HTML contains the Control UI title/root shell, then open Safari and require an established localhost TCP connection from Safari to the gateway port.
- For Tahoe `fresh.gateway-status`, prefer non-TTY `prlctl exec --current-user ... openclaw gateway status ...` plus a few short retries. `prlctl enter` can spam TTY control bytes and hang the phase log even when the CLI itself is healthy.
@@ -140,8 +140,8 @@ Use this skill for Parallels guest workflows and smoke interpretation. Do not lo
## Linux flow
- Preferred entrypoint: `pnpm test:parallels:linux`
- Use the snapshot closest to fresh `Ubuntu 24.04.3 ARM64`.
- If that exact VM is missing on the host, any Ubuntu guest with major version `>= 24` is acceptable; prefer the closest versioned Ubuntu guest with a fresh poweroff snapshot. On Peter's host today, that is `Ubuntu 25.10`.
- Use the newest versioned Ubuntu guest with a fresh poweroff snapshot. On Peter's host today, that is `Ubuntu 26.04`.
- If an exact requested Ubuntu VM is missing on the host, any Ubuntu guest with major version `>= 24` is acceptable; prefer the newest versioned Ubuntu guest over older fallback snapshots.
- Use plain `prlctl exec`; `--current-user` is not the right transport on this snapshot.
- Fresh snapshots may be missing `curl`, and `apt-get update` can fail on clock skew. Bootstrap with `apt-get -o Acquire::Check-Date=false update` and install `curl ca-certificates`.
- Fresh `main` tgz smoke still needs the latest-release installer first because the snapshot has no Node or npm before bootstrap.

View File

@@ -169,7 +169,9 @@ Output only qualifying candidates, with: ref, surface, proof, cause, fix sketch,
- Start every PR review with 1-3 plain sentences explaining what the change does and why it matters. Put this before `Findings`.
- Then list findings first. If none, say `No blocking findings` or `No findings`.
- Always answer: bug/behavior being fixed, PR/issue URL and affected surface, provenance for regressions when traceable, and best-fix verdict.
- For bug/regression fixes, include a compact `Provenance:` line after cause/root-cause when a bounded history pass can identify it. Use `git log -S/-G`, `git blame`, linked PRs/issues, and tests; separate author, committer/merger, and current PR author when they differ.
- For bug/regression fixes, include a compact `Provenance:` line after cause/root-cause when a bounded history pass can identify it. Use `git log -S/-G`, `git blame`, linked PRs/issues, and tests.
- Provenance must separate roles when they differ: blamed code author username, blamed PR merger/committer username, current PR author username, PR number, and date. Do not collapse them into one "introduced by" actor.
- For any confirmed bug, run `git blame` on the implicated line(s) after identifying the root cause. Report who broke it as the blamed PR merger/committer, and also name the blamed code author. Include the PR number. If no PR is traceable, use the blamed commit as the provenance: commit SHA, date, and author username. Do not guess a merger or frame missing PR metadata as a separate finding.
- Phrase provenance as `introduced by`, `made visible by`, or `carried forward by`, with confidence (`clear`, `likely`, `unknown`). If unclear, say what evidence is missing instead of guessing. For features, docs, and refactors, use `Provenance: N/A` or omit it when no broken behavior is being fixed.
- Keep summaries compact, but include enough proof that the verdict is auditable without rereading the PR.
@@ -192,7 +194,7 @@ Output only qualifying candidates, with: ref, surface, proof, cause, fix sketch,
- Before landing, require:
1. symptom evidence such as a repro, logs, or a failing test
2. a verified root cause in code with file/line
3. provenance for regressions when traceable by bounded git/PR history
3. blame-backed provenance for regressions when traceable, including blamed PR merger and date, or commit SHA/date when no PR is traceable
4. a fix that touches the implicated code path
5. a regression test when feasible, or explicit manual verification plus a reason no test was added
- If the claim is unsubstantiated or likely wrong, request evidence or changes instead of merging.

View File

@@ -18,6 +18,9 @@ capacity:
- us-west-2
actions:
workflow: .github/workflows/crabbox-hydrate.yml
# Default AWS hydration uses local Actions replay. Use
# `crabbox actions hydrate --github-runner --job hydrate-github` when the
# hydrate job needs GitHub secrets.
job: hydrate
ref: main
runnerLabels:

6
.github/CODEOWNERS vendored
View File

@@ -13,6 +13,12 @@
/.github/workflows/codeql-critical-quality.yml @openclaw/openclaw-secops
/.github/workflows/dependency-change-awareness.yml @openclaw/openclaw-secops
/test/scripts/dependency-change-awareness-workflow.test.ts @openclaw/openclaw-secops
/package-lock.json @openclaw/openclaw-secops
/npm-shrinkwrap.json @openclaw/openclaw-secops
/extensions/*/package-lock.json @openclaw/openclaw-secops
/extensions/*/npm-shrinkwrap.json @openclaw/openclaw-secops
/pnpm-lock.yaml @openclaw/openclaw-secops
/scripts/generate-npm-shrinkwrap.mjs @openclaw/openclaw-secops
/src/security/ @openclaw/openclaw-secops
/src/secrets/ @openclaw/openclaw-secops
/src/config/*secret*.ts @openclaw/openclaw-secops

View File

@@ -140,13 +140,33 @@ runs:
run: |
set -euo pipefail
credentials=",$CREDENTIALS,"
if [[ "$credentials" == *",openai,"* ]]; then
[[ -n "${OPENAI_API_KEY:-}" ]] || {
echo "OPENAI_API_KEY is required for selected Docker E2E lanes." >&2
exit 1
}
fi
if [[ "$credentials" == *",anthropic,"* && -z "${ANTHROPIC_API_TOKEN:-}" && -z "${ANTHROPIC_API_KEY:-}" ]]; then
echo "ANTHROPIC_API_TOKEN or ANTHROPIC_API_KEY is required for selected Docker E2E lanes." >&2
require_any() {
local label="$1"
shift
local key
for key in "$@"; do
if [[ -n "${!key:-}" ]]; then
return 0
fi
done
echo "Missing credential for ${label}: expected one of $*" >&2
exit 1
}
if [[ "$credentials" == *",openai,"* ]]; then
require_any OpenAI OPENAI_API_KEY
fi
if [[ "$credentials" == *",codex,"* ]]; then
require_any Codex OPENCLAW_CODEX_AUTH_JSON
fi
if [[ "$credentials" == *",anthropic,"* ]]; then
require_any Anthropic ANTHROPIC_API_TOKEN ANTHROPIC_API_KEY OPENCLAW_CLAUDE_CREDENTIALS_JSON OPENCLAW_CLAUDE_JSON
fi
if [[ "$credentials" == *",factory,"* ]]; then
require_any Factory FACTORY_API_KEY
fi
if [[ "$credentials" == *",gemini,"* ]]; then
require_any Gemini GEMINI_API_KEY GOOGLE_API_KEY OPENCLAW_GEMINI_SETTINGS_JSON
fi
if [[ "$credentials" == *",opencode,"* ]]; then
require_any OpenCode OPENCODE_API_KEY OPENCODE_ZEN_API_KEY
fi

View File

@@ -7,14 +7,6 @@ inputs:
description: Node.js version to install.
required: false
default: "24.x"
cache-key-suffix:
description: Suffix appended to the pnpm store cache key.
required: false
default: "node24-pnpm11"
pnpm-version:
description: pnpm version for corepack.
required: false
default: "11.0.8"
install-bun:
description: Whether to install Bun alongside Node.
required: false
@@ -27,6 +19,10 @@ inputs:
description: Whether to use --frozen-lockfile for install.
required: false
default: "true"
use-actions-cache:
description: Whether to restore and save the pnpm store with actions/cache.
required: false
default: "true"
runs:
using: composite
steps:
@@ -36,13 +32,11 @@ runs:
node-version: ${{ inputs.node-version }}
check-latest: false
- name: Setup pnpm + cache store
id: pnpm-cache
- name: Setup pnpm
uses: ./.github/actions/setup-pnpm-store-cache
with:
node-version: ${{ inputs.node-version }}
pnpm-version: ${{ inputs.pnpm-version }}
cache-key-suffix: ${{ inputs.cache-key-suffix }}
use-actions-cache: ${{ inputs.use-actions-cache }}
- name: Setup Bun
if: inputs.install-bun == 'true'
@@ -101,12 +95,25 @@ runs:
if [ -n "$LOCKFILE_FLAG" ]; then
install_args+=("$LOCKFILE_FLAG")
fi
append_pnpm_option_arg() {
local env_name="$1"
local option_name="$2"
local value="${!env_name-}"
if [ -n "$value" ]; then
install_args+=("--${option_name}=${value}")
fi
}
append_pnpm_option_arg PNPM_CONFIG_CHILD_CONCURRENCY child-concurrency
append_pnpm_option_arg PNPM_CONFIG_MODULES_DIR modules-dir
append_pnpm_option_arg PNPM_CONFIG_NETWORK_CONCURRENCY network-concurrency
append_pnpm_option_arg PNPM_CONFIG_VIRTUAL_STORE_DIR virtual-store-dir
if [ -n "${PNPM_CONFIG_MODULES_DIR:-}" ]; then
mkdir -p "$PNPM_CONFIG_MODULES_DIR"
ln -sfn . "$PNPM_CONFIG_MODULES_DIR/node_modules"
fi
pnpm "${install_args[@]}" || pnpm "${install_args[@]}"
- name: Save pnpm store cache
if: inputs.install-deps == 'true' && steps.pnpm-cache.outputs.cache-enabled == 'true' && steps.pnpm-cache.outputs.cache-hit != 'true'
uses: actions/cache/save@v5
continue-on-error: true
with:
path: ${{ steps.pnpm-cache.outputs.store-path }}
key: ${{ steps.pnpm-cache.outputs.primary-key }}
if [ -n "${PNPM_CONFIG_MODULES_DIR:-}" ]; then
rm -rf node_modules
ln -sfn "$PNPM_CONFIG_MODULES_DIR" node_modules
ln -sfn . "$PNPM_CONFIG_MODULES_DIR/node_modules"
fi

View File

@@ -1,168 +1,62 @@
name: Setup pnpm + store cache
description: Prepare pnpm via corepack and restore pnpm store cache.
name: Setup pnpm
description: Prepare pnpm from the repository packageManager and restore its store cache.
inputs:
pnpm-version:
description: pnpm version to activate via corepack.
package-manager-file:
description: package.json file that owns the packageManager pnpm pin.
required: false
default: "11.0.8"
default: "package.json"
lockfile-path:
description: pnpm lockfile used to key the store cache.
required: false
default: "pnpm-lock.yaml"
node-version:
description: Expected Node.js version already installed by actions/setup-node.
required: false
default: "24.x"
cache-key-suffix:
description: Suffix appended to the cache key.
required: false
default: "node24-pnpm11"
use-restore-keys:
description: Whether to use restore-keys fallback for actions/cache.
required: false
default: "true"
default: ""
use-actions-cache:
description: Whether to restore pnpm store with actions/cache.
description: Whether pnpm/action-setup should cache the pnpm store.
required: false
default: "true"
outputs:
cache-enabled:
description: Whether actions/cache restore was enabled.
value: ${{ steps.pnpm-cache-config.outputs.enabled }}
cache-hit:
description: Whether the pnpm store cache had an exact key hit.
value: ${{ steps.pnpm-cache-restore.outputs.cache-hit }}
cache-matched-key:
description: Cache key matched by restore, if any.
value: ${{ steps.pnpm-cache-restore.outputs.cache-matched-key }}
primary-key:
description: Primary pnpm store cache key.
value: ${{ steps.pnpm-cache-config.outputs.primary-key }}
store-path:
description: Resolved pnpm store path.
value: ${{ steps.pnpm-store.outputs.path }}
pnpm-version:
description: Resolved pnpm version activated by the setup action.
value: ${{ steps.pnpm-version.outputs.pnpm-version }}
project-dir:
description: Directory containing the packageManager file used for pnpm resolution.
value: ${{ steps.setup-pnpm.outputs.project-dir }}
runs:
using: composite
steps:
- name: Setup pnpm (corepack retry)
- name: Validate pnpm setup inputs
id: setup-pnpm
shell: bash
env:
COREPACK_ENABLE_DOWNLOAD_PROMPT: "0"
PNPM_VERSION: ${{ inputs.pnpm-version }}
PACKAGE_MANAGER_FILE: ${{ inputs.package-manager-file }}
REQUESTED_NODE_VERSION: ${{ inputs.node-version }}
run: |
set -euo pipefail
if [[ ! "$PNPM_VERSION" =~ ^[0-9]+(\.[0-9]+){1,2}([.-][0-9A-Za-z.-]+)?$ ]]; then
echo "::error::Invalid pnpm-version input: '$PNPM_VERSION'"
exit 2
fi
requested_node="${REQUESTED_NODE_VERSION:-${NODE_VERSION:-}}"
requested_node="${requested_node#v}"
node_version_matches() {
local actual="$1"
local requested="$2"
if [[ -z "$requested" ]]; then
return 0
fi
case "$requested" in
*x)
[[ "${actual%%.*}" == "${requested%%.*}" ]]
;;
*.*.*)
[[ "$actual" == "$requested" ]]
;;
*.*)
[[ "$actual" == "$requested".* ]]
;;
*)
[[ "${actual%%.*}" == "$requested" ]]
;;
esac
}
active_node_version="$(node -p 'process.versions.node' 2>/dev/null || true)"
if ! node_version_matches "$active_node_version" "$requested_node"; then
node_roots=()
for root in \
"${RUNNER_TOOL_CACHE:-}" \
"${AGENT_TOOLSDIRECTORY:-}" \
"${ACTIONS_RUNNER_TOOL_CACHE:-}" \
"/opt/hostedtoolcache" \
"/home/runner/_work/_tool" \
"/Users/runner/hostedtoolcache" \
"/c/hostedtoolcache/windows"
do
if [[ -d "$root/node" ]]; then
node_roots+=("$root/node")
elif [[ "$(basename "$root")" == "node" && -d "$root" ]]; then
node_roots+=("$root")
fi
done
node_bin=""
for node_root in "${node_roots[@]}"; do
while IFS= read -r candidate; do
candidate_version="$("$candidate" -p 'process.versions.node' 2>/dev/null || true)"
if node_version_matches "$candidate_version" "$requested_node"; then
node_bin="$candidate"
break 2
fi
done < <(find "$node_root" \( -name node -o -name node.exe \) -type f 2>/dev/null | sort -r)
done
if [[ -n "$node_bin" ]]; then
echo "Using Node $("$node_bin" -p 'process.versions.node') from $node_bin"
export PATH="$(dirname "$node_bin"):$PATH"
hash -r
fi
fi
active_node_version="$(node -p 'process.versions.node' 2>/dev/null || true)"
if ! node_version_matches "$active_node_version" "$requested_node"; then
echo "::error::Expected Node '${requested_node}', but active node is '${active_node_version:-missing}' at $(command -v node || true)"
project_dir="$(dirname "$PACKAGE_MANAGER_FILE")"
if [[ ! -f "$PACKAGE_MANAGER_FILE" ]]; then
echo "::error::package manager file not found: $PACKAGE_MANAGER_FILE"
exit 1
fi
echo "project-dir=$project_dir" >> "$GITHUB_OUTPUT"
node -v
command -v node
command -v corepack
corepack enable
for attempt in 1 2 3; do
if corepack prepare "pnpm@$PNPM_VERSION" --activate; then
pnpm -v
exit 0
fi
echo "corepack prepare failed (attempt $attempt/3). Retrying..."
sleep $((attempt * 10))
done
exit 1
requested_node="${REQUESTED_NODE_VERSION:-${NODE_VERSION:-}}"
source "$GITHUB_ACTION_PATH/ensure-node.sh"
openclaw_ensure_node "$requested_node"
- name: Resolve pnpm store path
id: pnpm-store
shell: bash
run: echo "path=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT"
- name: Setup pnpm from packageManager
uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093
with:
package_json_file: ${{ inputs.package-manager-file }}
run_install: false
cache: ${{ inputs.use-actions-cache }}
cache_dependency_path: ${{ inputs.lockfile-path }}
- name: Resolve pnpm store cache keys
id: pnpm-cache-config
- name: Record pnpm version
id: pnpm-version
shell: bash
env:
CACHE_KEY_SUFFIX: ${{ inputs.cache-key-suffix }}
LOCKFILE_HASH: ${{ hashFiles('pnpm-lock.yaml') }}
USE_ACTIONS_CACHE: ${{ inputs.use-actions-cache }}
USE_RESTORE_KEYS: ${{ inputs.use-restore-keys }}
run: |
set -euo pipefail
echo "enabled=$USE_ACTIONS_CACHE" >> "$GITHUB_OUTPUT"
echo "primary-key=${RUNNER_OS}-pnpm-store-${CACHE_KEY_SUFFIX}-${LOCKFILE_HASH}" >> "$GITHUB_OUTPUT"
if [ "$USE_RESTORE_KEYS" = "true" ]; then
echo "restore-keys=${RUNNER_OS}-pnpm-store-${CACHE_KEY_SUFFIX}-" >> "$GITHUB_OUTPUT"
else
echo "restore-keys=" >> "$GITHUB_OUTPUT"
fi
- name: Restore pnpm store cache
id: pnpm-cache-restore
if: inputs.use-actions-cache == 'true'
uses: actions/cache/restore@v5
with:
path: ${{ steps.pnpm-store.outputs.path }}
key: ${{ steps.pnpm-cache-config.outputs.primary-key }}
restore-keys: ${{ steps.pnpm-cache-config.outputs.restore-keys }}
PROJECT_DIR: ${{ steps.setup-pnpm.outputs.project-dir }}
run: echo "pnpm-version=$(cd "$PROJECT_DIR" && pnpm -v)" >> "$GITHUB_OUTPUT"

View File

@@ -0,0 +1,96 @@
#!/usr/bin/env bash
openclaw_node_version_matches() {
local actual="$1"
local requested="$2"
if [[ -z "$requested" ]]; then
return 0
fi
case "$requested" in
*x)
[[ "${actual%%.*}" == "${requested%%.*}" ]]
;;
*.*.*)
[[ "$actual" == "$requested" ]]
;;
*.*)
[[ "$actual" == "$requested".* ]]
;;
*)
[[ "${actual%%.*}" == "$requested" ]]
;;
esac
}
openclaw_active_node_version() {
node -p 'process.versions.node' 2>/dev/null || true
}
openclaw_prepend_node_bin() {
local node_bin_dir="$1"
export PATH="$node_bin_dir:$PATH"
if [[ -n "${GITHUB_PATH:-}" ]]; then
echo "$node_bin_dir" >> "$GITHUB_PATH"
fi
hash -r
}
openclaw_find_toolcache_node() {
local requested_node="$1"
local roots=()
local root
for root in \
"${RUNNER_TOOL_CACHE:-}" \
"${AGENT_TOOLSDIRECTORY:-}" \
"${ACTIONS_RUNNER_TOOL_CACHE:-}" \
"/opt/hostedtoolcache" \
"/home/runner/_work/_tool" \
"/Users/runner/hostedtoolcache" \
"/c/hostedtoolcache/windows"
do
if [[ -d "$root/node" ]]; then
roots+=("$root/node")
elif [[ "$(basename "$root")" == "node" && -d "$root" ]]; then
roots+=("$root")
fi
done
local node_root candidate candidate_version
for node_root in "${roots[@]}"; do
while IFS= read -r candidate; do
candidate_version="$("$candidate" -p 'process.versions.node' 2>/dev/null || true)"
if openclaw_node_version_matches "$candidate_version" "$requested_node"; then
printf '%s\n' "$candidate"
return 0
fi
done < <(find "$node_root" \( -name node -o -name node.exe \) -type f 2>/dev/null | sort -r)
done
return 1
}
openclaw_ensure_node() {
local requested_node="${1:-}"
requested_node="${requested_node#v}"
if [[ -z "$requested_node" ]]; then
return 0
fi
local active_node_version node_bin
active_node_version="$(openclaw_active_node_version)"
if openclaw_node_version_matches "$active_node_version" "$requested_node"; then
echo "Using active Node ${active_node_version} at $(command -v node)"
return 0
fi
node_bin="$(openclaw_find_toolcache_node "$requested_node" || true)"
if [[ -n "$node_bin" ]]; then
echo "Using Node $("$node_bin" -p 'process.versions.node') from $node_bin"
openclaw_prepend_node_bin "$(dirname "$node_bin")"
fi
active_node_version="$(openclaw_active_node_version)"
if ! openclaw_node_version_matches "$active_node_version" "$requested_node"; then
echo "::error::Expected Node '${requested_node}', but active node is '${active_node_version:-missing}' at $(command -v node || true)"
return 1
fi
}

6
.github/labeler.yml vendored
View File

@@ -36,6 +36,12 @@
- any-glob-to-any-file:
- "extensions/google-meet/**"
- "docs/plugins/google-meet.md"
"plugin: meeting-notes":
- changed-files:
- any-glob-to-any-file:
- "extensions/meeting-notes/**"
- "docs/plugins/meeting-notes.md"
- "src/meeting-notes/**"
"plugin: migrate-hermes":
- changed-files:
- any-glob-to-any-file:

4
.github/package-trusted-sources.json vendored Normal file
View File

@@ -0,0 +1,4 @@
{
"schemaVersion": 1,
"sources": {}
}

View File

@@ -1,165 +1,132 @@
## Summary
Describe the problem and fix in 25 bullets:
What problem does this PR solve?
Why does this matter now?
What is the intended outcome?
What is intentionally out of scope?
What does success look like?
What should reviewers focus on?
<details>
<summary>Summary guidance</summary>
This PR description is the contributor's durable explanation of the change. Write it for human maintainers first; ClawSweeper and Barnacle use the same text to understand intent, proof, risk, and current review state.
Describe the intent and outcome in 2-5 bullets. Avoid restating the diff; reviewers and bots can read the changed files.
If this PR fixes a plugin beta-release blocker, title it `fix(<plugin-id>): beta blocker - <summary>` and link the matching `Beta blocker: <plugin-name> - <summary>` issue labeled `beta-blocker`. Contributors cannot label PRs, so the title is the PR-side signal for maintainers and automation.
- Problem:
- Solution:
- What changed:
- What did NOT change (scope boundary):
</details>
## Motivation
## Linked context
Explain why this change should exist now. Link it to the user pain, failure mode, maintainer need, or product goal. If this is purely mechanical, write `N/A`.
Which issue does this close?
-
Closes #
## Change Type (select all)
Which issues, PRs, or discussions are related?
- [ ] Bug fix
- [ ] Feature
- [ ] Refactor required for the fix
- [ ] Docs
- [ ] Security hardening
- [ ] Chore/infra
Related #
## Scope (select all touched areas)
Was this requested by a maintainer or owner?
- [ ] Gateway / orchestration
- [ ] Skills / tool execution
- [ ] Auth / tokens
- [ ] Memory / storage
- [ ] Integrations
- [ ] API / contracts
- [ ] UI / DX
- [ ] CI/CD / infra
<details>
<summary>Linked context guidance</summary>
## Linked Issue/PR
Link the issue, PR, discussion, maintainer request, or owner request that explains why this PR should exist. Maintainer context helps reviewers and automation distinguish intended work from drive-by churn.
- Closes #
- Related #
- [ ] This PR fixes a bug or regression
</details>
## Real behavior proof (required for external PRs)
External contributors must show after-fix evidence from a real OpenClaw setup. Unit tests, mocks, lint, typechecks, snapshots, and CI are supplemental only. Screenshots are encouraged even for CLI, console, text, or log changes; terminal screenshots and copied live output count. Be mindful of private information like IP addresses, API keys, phone numbers, non-public endpoints, or other private details when providing evidence.
- Behavior or issue addressed:
- Real environment tested:
- Exact steps or command run after this patch:
- Evidence after fix (screenshot, recording, terminal capture, console output, redacted runtime log, linked artifact, or copied live output):
- Observed result after fix:
- What was not tested:
- Proof limitations or environment constraints:
- Before evidence (optional but encouraged):
## Root Cause (if applicable)
<details>
<summary>Real behavior proof guidance</summary>
For bug fixes or regressions, explain why this happened, not just what changed. Otherwise write `N/A`. If the cause is unclear, write `Unknown`.
External contributors must show after-fix evidence from a real OpenClaw setup. Unit tests, mocks, lint, typechecks, snapshots, and CI are supplemental only.
- Root cause:
- Missing detection / guardrail:
- Contributing context (if known):
Screenshots are encouraged even for CLI, console, text, or log changes. Terminal screenshots, copied live output, redacted runtime logs, recordings, and linked artifacts count.
## Regression Test Plan (if applicable)
If your environment cannot produce the ideal proof, explain that under `Proof limitations or environment constraints` so reviewers and ClawSweeper can direct the next step properly.
For bug fixes or regressions, name the smallest reliable test coverage that should catch this. Otherwise write `N/A`.
Be mindful of private information like IP addresses, API keys, phone numbers, non-public endpoints, or other private details when providing evidence.
- Coverage level that should have caught this:
- [ ] Unit test
- [ ] Seam / integration test
- [ ] End-to-end test
- [ ] Existing coverage already sufficient
- Target test or file:
- Scenario the test should lock in:
- Why this is the smallest reliable guardrail:
- Existing test that already covers this (if any):
- If no new test is added, why not:
</details>
## User-visible / Behavior Changes
## Tests and validation
List user-visible changes (including defaults/config).
If none, write `None`.
Which commands did you run?
## Diagram (if applicable)
For UI changes or non-trivial logic flows, include a small ASCII diagram reviewers can scan quickly. Otherwise write `N/A`.
What regression coverage was added or updated?
```text
Before:
[user action] -> [old state]
After:
[user action] -> [new state] -> [result]
```
What failed before this fix, if known?
## Security Impact (required)
- New permissions/capabilities? (`Yes/No`)
- Secrets/tokens handling changed? (`Yes/No`)
- New/changed network calls? (`Yes/No`)
- Command/tool execution surface changed? (`Yes/No`)
- Data access scope changed? (`Yes/No`)
- If any `Yes`, explain risk + mitigation:
If no test was added, why not?
## Repro + Verification
<details>
<summary>Testing guidance</summary>
### Environment
List focused commands, not every incidental check. CI is useful support, but external PRs still need real behavior proof above when behavior changes.
- OS:
- Runtime/container:
- Model/provider:
- Integration/channel (if any):
- Relevant config (redacted):
</details>
### Steps
## Risk checklist
1.
2.
3.
Did user-visible behavior change? (`Yes/No`)
### Expected
-
Did config, environment, or migration behavior change? (`Yes/No`)
### Actual
-
Did security, auth, secrets, network, or tool execution behavior change? (`Yes/No`)
## Evidence
Attach at least one:
What is the highest-risk area?
- [ ] Failing test/log before + passing after
- [ ] Trace/log snippets
- [ ] Screenshot/recording
- [ ] Perf numbers (if relevant)
## Human Verification (required)
How is that risk mitigated?
What you personally verified (not just CI), and how:
<details>
<summary>Risk guidance</summary>
- Verified scenarios:
- Edge cases checked:
- What you did **not** verify:
Use this for author judgment that is not obvious from the diff. ClawSweeper can see touched files, but it cannot know which behavior you think is risky, why the risk is acceptable, or what mitigation reviewers should verify.
## Review Conversations
</details>
- [ ] I replied to or resolved every bot review conversation I addressed in this PR.
- [ ] I left unresolved only the conversations that still need reviewer or maintainer judgment.
## Current review state
If a bot review conversation is addressed by this PR, resolve that conversation yourself. Do not leave bot review conversation cleanup for maintainers.
What is the next action?
## Compatibility / Migration
- Backward compatible? (`Yes/No`)
- Config/env changes? (`Yes/No`)
- Migration needed? (`Yes/No`)
- If yes, exact upgrade steps:
What is still waiting on author, maintainer, CI, or external proof?
## Risks and Mitigations
List only real risks for this PR. Add/remove entries as needed. If none, write `None`.
Which bot or reviewer comments were addressed?
- Risk:
- Mitigation:
<details>
<summary>Review state guidance</summary>
Keep this as the durable state for review progress. If useful information appears in comments, fold the current next action or blocker back here so maintainers and ClawSweeper do not need to reconstruct state from comment history.
</details>

View File

@@ -41,6 +41,10 @@ jobs:
set -euo pipefail
workdir="$GITHUB_WORKSPACE"
if [[ -z "$CHECKOUT_TOKEN" ]]; then
echo "checkout token is missing" >&2
exit 1
fi
auth_header="$(printf 'x-access-token:%s' "$CHECKOUT_TOKEN" | base64 | tr -d '\n')"
reset_checkout_dir() {
@@ -59,7 +63,7 @@ jobs:
timeout --signal=TERM 30s git -C "$workdir" \
-c protocol.version=2 \
-c "http.https://github.com/.extraheader=AUTHORIZATION: basic ${auth_header}" \
-c "http.extraheader=AUTHORIZATION: basic ${auth_header}" \
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
"+${CHECKOUT_SHA}:refs/remotes/origin/ci-target" || return 1
@@ -187,12 +191,15 @@ jobs:
git fetch --no-tags --depth=50 origin "+refs/heads/main:refs/remotes/origin/main"
node_bin="$(dirname "$(node -p 'process.execPath')")"
pnpm_bin="$(command -v pnpm)"
sudo ln -sf "$node_bin/node" /usr/local/bin/node
sudo ln -sf "$node_bin/npm" /usr/local/bin/npm
sudo ln -sf "$node_bin/npx" /usr/local/bin/npx
sudo ln -sf "$node_bin/corepack" /usr/local/bin/corepack
sudo ln -sf "$pnpm_bin" /usr/local/bin/pnpm
sudo tee /usr/local/bin/pnpm >/dev/null <<'PNPM'
#!/usr/bin/env bash
exec /usr/local/bin/corepack pnpm "$@"
PNPM
sudo chmod 0755 /usr/local/bin/pnpm
- name: Hydrate Testbox provider env helper
shell: bash
@@ -222,6 +229,6 @@ jobs:
- name: Run Testbox
uses: useblacksmith/run-testbox@5ca05834db1d3813554d1dd109e5f2087a8d7cbc
if: always()
if: success()
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"

View File

@@ -39,6 +39,10 @@ jobs:
set -euo pipefail
workdir="$GITHUB_WORKSPACE"
if [[ -z "$CHECKOUT_TOKEN" ]]; then
echo "checkout token is missing" >&2
exit 1
fi
auth_header="$(printf 'x-access-token:%s' "$CHECKOUT_TOKEN" | base64 | tr -d '\n')"
reset_checkout_dir() {
@@ -57,7 +61,7 @@ jobs:
timeout --signal=TERM 30s git -C "$workdir" \
-c protocol.version=2 \
-c "http.https://github.com/.extraheader=AUTHORIZATION: basic ${auth_header}" \
-c "http.extraheader=AUTHORIZATION: basic ${auth_header}" \
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
"+${CHECKOUT_SHA}:refs/remotes/origin/ci-target" || return 1
@@ -88,12 +92,15 @@ jobs:
git fetch --no-tags --depth=50 origin "+refs/heads/main:refs/remotes/origin/main"
node_bin="$(dirname "$(node -p 'process.execPath')")"
pnpm_bin="$(command -v pnpm)"
sudo ln -sf "$node_bin/node" /usr/local/bin/node
sudo ln -sf "$node_bin/npm" /usr/local/bin/npm
sudo ln -sf "$node_bin/npx" /usr/local/bin/npx
sudo ln -sf "$node_bin/corepack" /usr/local/bin/corepack
sudo ln -sf "$pnpm_bin" /usr/local/bin/pnpm
sudo tee /usr/local/bin/pnpm >/dev/null <<'PNPM'
#!/usr/bin/env bash
exec /usr/local/bin/corepack pnpm "$@"
PNPM
sudo chmod 0755 /usr/local/bin/pnpm
- name: Hydrate Testbox provider env helper
shell: bash
@@ -103,6 +110,7 @@ jobs:
ANTHROPIC_API_TOKEN: ${{ secrets.ANTHROPIC_API_TOKEN }}
CEREBRAS_API_KEY: ${{ secrets.CEREBRAS_API_KEY }}
DEEPINFRA_API_KEY: ${{ secrets.DEEPINFRA_API_KEY }}
FACTORY_API_KEY: ${{ secrets.FACTORY_API_KEY }}
FIREWORKS_API_KEY: ${{ secrets.FIREWORKS_API_KEY }}
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
@@ -123,7 +131,7 @@ jobs:
- name: Run Testbox
uses: useblacksmith/run-testbox@5ca05834db1d3813554d1dd109e5f2087a8d7cbc
if: always()
if: success()
continue-on-error: true
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"

View File

@@ -81,7 +81,7 @@ jobs:
ref: ${{ inputs.target_ref || github.sha }}
fetch-depth: 1
fetch-tags: false
persist-credentials: false
persist-credentials: true
submodules: false
- name: Resolve checkout SHA
@@ -304,7 +304,7 @@ jobs:
ref: ${{ inputs.target_ref || github.sha }}
fetch-depth: 1
fetch-tags: false
persist-credentials: false
persist-credentials: true
submodules: false
- name: Ensure security base commit
@@ -416,8 +416,6 @@ jobs:
set -euo pipefail
workdir="$GITHUB_WORKSPACE"
auth_header="$(printf 'x-access-token:%s' "$CHECKOUT_TOKEN" | base64 | tr -d '\n')"
reset_checkout_dir() {
mkdir -p "$workdir"
find "$workdir" -mindepth 1 -maxdepth 1 -exec rm -rf {} +
@@ -429,12 +427,11 @@ jobs:
reset_checkout_dir
git init "$workdir" >/dev/null
git config --global --add safe.directory "$workdir"
git -C "$workdir" remote add origin "https://github.com/${CHECKOUT_REPO}"
git -C "$workdir" remote add origin "https://x-access-token:${CHECKOUT_TOKEN}@github.com/${CHECKOUT_REPO}.git"
git -C "$workdir" config gc.auto 0
timeout --signal=TERM 30s git -C "$workdir" \
-c protocol.version=2 \
-c "http.https://github.com/.extraheader=AUTHORIZATION: basic ${auth_header}" \
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
"+${CHECKOUT_SHA}:refs/remotes/origin/ci-target" || return 1
@@ -627,8 +624,6 @@ jobs:
set -euo pipefail
workdir="$GITHUB_WORKSPACE"
auth_header="$(printf 'x-access-token:%s' "$CHECKOUT_TOKEN" | base64 | tr -d '\n')"
reset_checkout_dir() {
mkdir -p "$workdir"
find "$workdir" -mindepth 1 -maxdepth 1 -exec rm -rf {} +
@@ -640,12 +635,11 @@ jobs:
reset_checkout_dir
git init "$workdir" >/dev/null
git config --global --add safe.directory "$workdir"
git -C "$workdir" remote add origin "https://github.com/${CHECKOUT_REPO}"
git -C "$workdir" remote add origin "https://x-access-token:${CHECKOUT_TOKEN}@github.com/${CHECKOUT_REPO}.git"
git -C "$workdir" config gc.auto 0
timeout --signal=TERM 30s git -C "$workdir" \
-c protocol.version=2 \
-c "http.https://github.com/.extraheader=AUTHORIZATION: basic ${auth_header}" \
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
"+${CHECKOUT_SHA}:refs/remotes/origin/ci-target" || return 1
@@ -717,8 +711,6 @@ jobs:
set -euo pipefail
workdir="$GITHUB_WORKSPACE"
auth_header="$(printf 'x-access-token:%s' "$CHECKOUT_TOKEN" | base64 | tr -d '\n')"
reset_checkout_dir() {
mkdir -p "$workdir"
find "$workdir" -mindepth 1 -maxdepth 1 -exec rm -rf {} +
@@ -730,12 +722,11 @@ jobs:
reset_checkout_dir
git init "$workdir" >/dev/null
git config --global --add safe.directory "$workdir"
git -C "$workdir" remote add origin "https://github.com/${CHECKOUT_REPO}"
git -C "$workdir" remote add origin "https://x-access-token:${CHECKOUT_TOKEN}@github.com/${CHECKOUT_REPO}.git"
git -C "$workdir" config gc.auto 0
timeout --signal=TERM 30s git -C "$workdir" \
-c protocol.version=2 \
-c "http.https://github.com/.extraheader=AUTHORIZATION: basic ${auth_header}" \
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
"+${CHECKOUT_SHA}:refs/remotes/origin/ci-target" || return 1
@@ -801,8 +792,6 @@ jobs:
set -euo pipefail
workdir="$GITHUB_WORKSPACE"
auth_header="$(printf 'x-access-token:%s' "$CHECKOUT_TOKEN" | base64 | tr -d '\n')"
reset_checkout_dir() {
mkdir -p "$workdir"
find "$workdir" -mindepth 1 -maxdepth 1 -exec rm -rf {} +
@@ -814,12 +803,11 @@ jobs:
reset_checkout_dir
git init "$workdir" >/dev/null
git config --global --add safe.directory "$workdir"
git -C "$workdir" remote add origin "https://github.com/${CHECKOUT_REPO}"
git -C "$workdir" remote add origin "https://x-access-token:${CHECKOUT_TOKEN}@github.com/${CHECKOUT_REPO}.git"
git -C "$workdir" config gc.auto 0
timeout --signal=TERM 30s git -C "$workdir" \
-c protocol.version=2 \
-c "http.https://github.com/.extraheader=AUTHORIZATION: basic ${auth_header}" \
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
"+${CHECKOUT_SHA}:refs/remotes/origin/ci-target" || return 1
@@ -882,8 +870,6 @@ jobs:
set -euo pipefail
workdir="$GITHUB_WORKSPACE"
auth_header="$(printf 'x-access-token:%s' "$CHECKOUT_TOKEN" | base64 | tr -d '\n')"
reset_checkout_dir() {
mkdir -p "$workdir"
find "$workdir" -mindepth 1 -maxdepth 1 -exec rm -rf {} +
@@ -895,12 +881,11 @@ jobs:
reset_checkout_dir
git init "$workdir" >/dev/null
git config --global --add safe.directory "$workdir"
git -C "$workdir" remote add origin "https://github.com/${CHECKOUT_REPO}"
git -C "$workdir" remote add origin "https://x-access-token:${CHECKOUT_TOKEN}@github.com/${CHECKOUT_REPO}.git"
git -C "$workdir" config gc.auto 0
timeout --signal=TERM 30s git -C "$workdir" \
-c protocol.version=2 \
-c "http.https://github.com/.extraheader=AUTHORIZATION: basic ${auth_header}" \
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
"+${CHECKOUT_SHA}:refs/remotes/origin/ci-target" || return 1
@@ -924,7 +909,6 @@ jobs:
uses: ./.github/actions/setup-node-env
with:
node-version: "22.19.0"
cache-key-suffix: "node22-pnpm11"
install-bun: "false"
- name: Configure Node test resources
@@ -962,8 +946,6 @@ jobs:
set -euo pipefail
workdir="$GITHUB_WORKSPACE"
auth_header="$(printf 'x-access-token:%s' "$CHECKOUT_TOKEN" | base64 | tr -d '\n')"
reset_checkout_dir() {
mkdir -p "$workdir"
find "$workdir" -mindepth 1 -maxdepth 1 -exec rm -rf {} +
@@ -975,12 +957,11 @@ jobs:
reset_checkout_dir
git init "$workdir" >/dev/null
git config --global --add safe.directory "$workdir"
git -C "$workdir" remote add origin "https://github.com/${CHECKOUT_REPO}"
git -C "$workdir" remote add origin "https://x-access-token:${CHECKOUT_TOKEN}@github.com/${CHECKOUT_REPO}.git"
git -C "$workdir" config gc.auto 0
timeout --signal=TERM 30s git -C "$workdir" \
-c protocol.version=2 \
-c "http.https://github.com/.extraheader=AUTHORIZATION: basic ${auth_header}" \
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
"+${CHECKOUT_SHA}:refs/remotes/origin/ci-target" || return 1
@@ -1004,7 +985,6 @@ jobs:
uses: ./.github/actions/setup-node-env
with:
node-version: "${{ matrix.node_version || '24.x' }}"
cache-key-suffix: "${{ matrix.cache_key_suffix || 'node24-pnpm11' }}"
install-bun: "false"
- name: Configure Node test resources
@@ -1089,8 +1069,6 @@ jobs:
set -euo pipefail
workdir="$GITHUB_WORKSPACE"
auth_header="$(printf 'x-access-token:%s' "$CHECKOUT_TOKEN" | base64 | tr -d '\n')"
reset_checkout_dir() {
mkdir -p "$workdir"
find "$workdir" -mindepth 1 -maxdepth 1 -exec rm -rf {} +
@@ -1102,12 +1080,11 @@ jobs:
reset_checkout_dir
git init "$workdir" >/dev/null
git config --global --add safe.directory "$workdir"
git -C "$workdir" remote add origin "https://github.com/${CHECKOUT_REPO}"
git -C "$workdir" remote add origin "https://x-access-token:${CHECKOUT_TOKEN}@github.com/${CHECKOUT_REPO}.git"
git -C "$workdir" config gc.auto 0
timeout --signal=TERM 30s git -C "$workdir" \
-c protocol.version=2 \
-c "http.https://github.com/.extraheader=AUTHORIZATION: basic ${auth_header}" \
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
"+${CHECKOUT_SHA}:refs/remotes/origin/ci-target" || return 1
@@ -1145,6 +1122,7 @@ jobs:
pnpm tool-display:check
pnpm check:host-env-policy:swift
pnpm dup:check:coverage
pnpm deps:shrinkwrap:check
pnpm deps:patches:check
pnpm lint:webhook:no-low-level-body-read
pnpm lint:auth:no-pairing-store-group
@@ -1222,8 +1200,6 @@ jobs:
set -euo pipefail
workdir="$GITHUB_WORKSPACE"
auth_header="$(printf 'x-access-token:%s' "$CHECKOUT_TOKEN" | base64 | tr -d '\n')"
reset_checkout_dir() {
mkdir -p "$workdir"
find "$workdir" -mindepth 1 -maxdepth 1 -exec rm -rf {} +
@@ -1235,12 +1211,11 @@ jobs:
reset_checkout_dir
git init "$workdir" >/dev/null
git config --global --add safe.directory "$workdir"
git -C "$workdir" remote add origin "https://github.com/${CHECKOUT_REPO}"
git -C "$workdir" remote add origin "https://x-access-token:${CHECKOUT_TOKEN}@github.com/${CHECKOUT_REPO}.git"
git -C "$workdir" config gc.auto 0
timeout --signal=TERM 30s git -C "$workdir" \
-c protocol.version=2 \
-c "http.https://github.com/.extraheader=AUTHORIZATION: basic ${auth_header}" \
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
"+${CHECKOUT_SHA}:refs/remotes/origin/ci-target" || return 1
@@ -1375,8 +1350,6 @@ jobs:
set -euo pipefail
workdir="$GITHUB_WORKSPACE"
auth_header="$(printf 'x-access-token:%s' "$CHECKOUT_TOKEN" | base64 | tr -d '\n')"
reset_checkout_dir() {
mkdir -p "$workdir"
find "$workdir" -mindepth 1 -maxdepth 1 -exec rm -rf {} +
@@ -1388,12 +1361,11 @@ jobs:
reset_checkout_dir
git init "$workdir" >/dev/null
git config --global --add safe.directory "$workdir"
git -C "$workdir" remote add origin "https://github.com/${CHECKOUT_REPO}"
git -C "$workdir" remote add origin "https://x-access-token:${CHECKOUT_TOKEN}@github.com/${CHECKOUT_REPO}.git"
git -C "$workdir" config gc.auto 0
timeout --signal=TERM 30s git -C "$workdir" \
-c protocol.version=2 \
-c "http.https://github.com/.extraheader=AUTHORIZATION: basic ${auth_header}" \
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
"+${CHECKOUT_SHA}:refs/remotes/origin/ci-target" || return 1
@@ -1424,7 +1396,7 @@ jobs:
repository: openclaw/clawhub
path: clawhub-source
fetch-depth: 1
persist-credentials: false
persist-credentials: true
- name: Check docs
env:
@@ -1443,7 +1415,7 @@ jobs:
uses: actions/checkout@v6
with:
ref: ${{ needs.preflight.outputs.checkout_revision }}
persist-credentials: false
persist-credentials: true
submodules: false
- name: Setup Python
@@ -1486,7 +1458,7 @@ jobs:
uses: actions/checkout@v6
with:
ref: ${{ needs.preflight.outputs.checkout_revision }}
persist-credentials: false
persist-credentials: true
submodules: false
- name: Try to exclude workspace from Windows Defender (best-effort)
@@ -1514,14 +1486,10 @@ jobs:
node-version: 24.x
check-latest: false
- name: Setup pnpm + cache store
id: pnpm-cache
- name: Setup pnpm
uses: ./.github/actions/setup-pnpm-store-cache
with:
pnpm-version: "11.0.8"
cache-key-suffix: "node24-pnpm11"
use-restore-keys: "false"
use-actions-cache: "true"
node-version: 24.x
- name: Runtime versions
run: |
@@ -1549,14 +1517,6 @@ jobs:
# caches can skip repeated rebuild/download work on later shards/runs.
pnpm install --frozen-lockfile --prefer-offline --ignore-scripts=false --config.engine-strict=false --config.enable-pre-post-scripts=true --config.side-effects-cache=true || pnpm install --frozen-lockfile --prefer-offline --ignore-scripts=false --config.engine-strict=false --config.enable-pre-post-scripts=true --config.side-effects-cache=true
- name: Save pnpm store cache
if: steps.pnpm-cache.outputs.cache-enabled == 'true' && steps.pnpm-cache.outputs.cache-hit != 'true'
uses: actions/cache/save@v5
continue-on-error: true
with:
path: ${{ steps.pnpm-cache.outputs.store-path }}
key: ${{ steps.pnpm-cache.outputs.primary-key }}
- name: Run ${{ matrix.task }} (${{ matrix.runtime }})
env:
TASK: ${{ matrix.task }}
@@ -1591,7 +1551,7 @@ jobs:
uses: actions/checkout@v6
with:
ref: ${{ needs.preflight.outputs.checkout_revision }}
persist-credentials: false
persist-credentials: true
submodules: false
- name: Setup Node environment
@@ -1632,7 +1592,7 @@ jobs:
uses: actions/checkout@v6
with:
ref: ${{ needs.preflight.outputs.checkout_revision }}
persist-credentials: false
persist-credentials: true
submodules: false
- name: Install XcodeGen / SwiftLint / SwiftFormat
@@ -1738,8 +1698,6 @@ jobs:
set -euo pipefail
workdir="$GITHUB_WORKSPACE"
auth_header="$(printf 'x-access-token:%s' "$CHECKOUT_TOKEN" | base64 | tr -d '\n')"
reset_checkout_dir() {
mkdir -p "$workdir"
find "$workdir" -mindepth 1 -maxdepth 1 -exec rm -rf {} +
@@ -1751,12 +1709,11 @@ jobs:
reset_checkout_dir
git init "$workdir" >/dev/null
git config --global --add safe.directory "$workdir"
git -C "$workdir" remote add origin "https://github.com/${CHECKOUT_REPO}"
git -C "$workdir" remote add origin "https://x-access-token:${CHECKOUT_TOKEN}@github.com/${CHECKOUT_REPO}.git"
git -C "$workdir" config gc.auto 0
timeout --signal=TERM 30s git -C "$workdir" \
-c protocol.version=2 \
-c "http.https://github.com/.extraheader=AUTHORIZATION: basic ${auth_header}" \
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
"+${CHECKOUT_SHA}:refs/remotes/origin/ci-target" || return 1

View File

@@ -31,10 +31,17 @@ permissions:
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
PNPM_CONFIG_CHILD_CONCURRENCY: "1"
PNPM_CONFIG_MODULES_DIR: "/tmp/openclaw-pnpm-node-modules"
PNPM_CONFIG_NETWORK_CONCURRENCY: "1"
PNPM_CONFIG_STORE_DIR: "/tmp/openclaw-pnpm-store"
PNPM_CONFIG_VERIFY_DEPS_BEFORE_RUN: "false"
PNPM_CONFIG_VIRTUAL_STORE_DIR: "/tmp/openclaw-pnpm-virtual-store"
jobs:
hydrate:
name: hydrate
if: ${{ inputs.crabbox_job != 'hydrate-github' }}
runs-on: [self-hosted, "${{ inputs.crabbox_runner_label }}"]
timeout-minutes: 120
steps:
@@ -42,25 +49,90 @@ jobs:
with:
ref: ${{ inputs.ref || github.ref }}
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
- name: Setup Node.js
uses: actions/setup-node@v6
with:
install-bun: "false"
node-version: "24"
- name: Setup pnpm and dependencies
shell: bash
env:
CI: "true"
run: |
set -euo pipefail
export XDG_CACHE_HOME="${XDG_CACHE_HOME:-$RUNNER_TEMP/cache}"
export COREPACK_HOME="${COREPACK_HOME:-$XDG_CACHE_HOME/corepack}"
export PNPM_HOME="${PNPM_HOME:-$RUNNER_TEMP/pnpm-home}"
mkdir -p "$XDG_CACHE_HOME" "$COREPACK_HOME" "$PNPM_HOME"
export PATH="$PNPM_HOME:$PATH"
{
echo "XDG_CACHE_HOME=$XDG_CACHE_HOME"
echo "COREPACK_HOME=$COREPACK_HOME"
echo "PNPM_HOME=$PNPM_HOME"
} >> "$GITHUB_ENV"
corepack enable --install-directory "$PNPM_HOME"
node_bin="$(dirname "$(node -p 'process.execPath')")"
echo "NODE_BIN=$node_bin" >> "$GITHUB_ENV"
echo "$node_bin" >> "$GITHUB_PATH"
export PATH="$node_bin:$PATH"
node -v
npm -v
pnpm -v
install_args=(
install
--prefer-offline
--ignore-scripts=false
--config.engine-strict=false
--config.enable-pre-post-scripts=true
--config.side-effects-cache=true
--frozen-lockfile
)
append_pnpm_option_arg() {
local env_name="$1"
local option_name="$2"
local value="${!env_name-}"
if [ -n "$value" ]; then
install_args+=("--${option_name}=${value}")
fi
}
append_pnpm_option_arg PNPM_CONFIG_CHILD_CONCURRENCY child-concurrency
append_pnpm_option_arg PNPM_CONFIG_MODULES_DIR modules-dir
append_pnpm_option_arg PNPM_CONFIG_NETWORK_CONCURRENCY network-concurrency
append_pnpm_option_arg PNPM_CONFIG_VIRTUAL_STORE_DIR virtual-store-dir
if [ -n "${PNPM_CONFIG_MODULES_DIR:-}" ]; then
mkdir -p "$PNPM_CONFIG_MODULES_DIR"
ln -sfn . "$PNPM_CONFIG_MODULES_DIR/node_modules"
fi
pnpm "${install_args[@]}" || pnpm "${install_args[@]}"
if [ -n "${PNPM_CONFIG_MODULES_DIR:-}" ]; then
rm -rf node_modules
ln -sfn "$PNPM_CONFIG_MODULES_DIR" node_modules
ln -sfn . "$PNPM_CONFIG_MODULES_DIR/node_modules"
fi
- name: Prepare Crabbox shell
shell: bash
run: |
set -euo pipefail
git fetch --no-tags --depth=50 origin "+refs/heads/main:refs/remotes/origin/main"
if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
git fetch --no-tags --depth=50 origin "+refs/heads/main:refs/remotes/origin/main"
fi
node_bin="$(dirname "$(node -p 'process.execPath')")"
pnpm_bin="$(command -v pnpm)"
sudo ln -sf "$node_bin/node" /usr/local/bin/node
sudo ln -sf "$node_bin/npm" /usr/local/bin/npm
sudo ln -sf "$node_bin/npx" /usr/local/bin/npx
sudo ln -sf "$node_bin/corepack" /usr/local/bin/corepack
sudo ln -sf "$pnpm_bin" /usr/local/bin/pnpm
sudo tee /usr/local/bin/pnpm >/dev/null <<'PNPM'
#!/usr/bin/env bash
exec /usr/local/bin/corepack pnpm "$@"
PNPM
sudo chmod 0755 /usr/local/bin/pnpm
- name: Ensure Docker is running
shell: bash
@@ -85,34 +157,37 @@ jobs:
sudo chmod 666 /var/run/docker.sock
fi
if ! docker buildx version >/dev/null 2>&1; then
arch="$(uname -m)"
case "$arch" in
aarch64|arm64) buildx_arch=arm64 ;;
x86_64|amd64) buildx_arch=amd64 ;;
*) echo "unsupported buildx arch: $arch" >&2; exit 2 ;;
esac
buildx_version="${DOCKER_BUILDX_VERSION:-v0.15.1}"
mkdir -p "$HOME/.docker/cli-plugins"
curl -fsSL \
"https://github.com/docker/buildx/releases/download/${buildx_version}/buildx-${buildx_version}.linux-${buildx_arch}" \
-o "$HOME/.docker/cli-plugins/docker-buildx"
chmod 0755 "$HOME/.docker/cli-plugins/docker-buildx"
fi
docker version
docker buildx version || true
docker buildx version
docker compose version || true
- name: Ensure SSH is available
shell: bash
run: |
set -euo pipefail
if command -v systemctl >/dev/null 2>&1; then
sudo systemctl start ssh || sudo systemctl start sshd || true
elif command -v service >/dev/null 2>&1; then
sudo service ssh start || sudo service sshd start || true
fi
- name: Hydrate provider env helper
shell: bash
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
ANTHROPIC_API_KEY_OLD: ${{ secrets.ANTHROPIC_API_KEY_OLD }}
ANTHROPIC_API_TOKEN: ${{ secrets.ANTHROPIC_API_TOKEN }}
CEREBRAS_API_KEY: ${{ secrets.CEREBRAS_API_KEY }}
DEEPINFRA_API_KEY: ${{ secrets.DEEPINFRA_API_KEY }}
FIREWORKS_API_KEY: ${{ secrets.FIREWORKS_API_KEY }}
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}
KIMI_API_KEY: ${{ secrets.KIMI_API_KEY }}
MINIMAX_API_KEY: ${{ secrets.MINIMAX_API_KEY }}
MISTRAL_API_KEY: ${{ secrets.MISTRAL_API_KEY }}
MOONSHOT_API_KEY: ${{ secrets.MOONSHOT_API_KEY }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL }}
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
QWEN_API_KEY: ${{ secrets.QWEN_API_KEY }}
TOGETHER_API_KEY: ${{ secrets.TOGETHER_API_KEY }}
XAI_API_KEY: ${{ secrets.XAI_API_KEY }}
ZAI_API_KEY: ${{ secrets.ZAI_API_KEY }}
Z_AI_API_KEY: ${{ secrets.Z_AI_API_KEY }}
run: bash scripts/ci-hydrate-testbox-env.sh
- name: Mark Crabbox ready
@@ -142,7 +217,196 @@ jobs:
fi
}
{
for key in CI GITHUB_ACTIONS GITHUB_WORKSPACE GITHUB_REPOSITORY GITHUB_RUN_ID GITHUB_RUN_NUMBER GITHUB_RUN_ATTEMPT GITHUB_REF GITHUB_REF_NAME GITHUB_SHA GITHUB_EVENT_NAME GITHUB_ACTOR RUNNER_OS RUNNER_ARCH RUNNER_TEMP RUNNER_TOOL_CACHE; do
for key in CI GITHUB_ACTIONS GITHUB_WORKSPACE GITHUB_REPOSITORY GITHUB_RUN_ID GITHUB_RUN_NUMBER GITHUB_RUN_ATTEMPT GITHUB_REF GITHUB_REF_NAME GITHUB_SHA GITHUB_EVENT_NAME GITHUB_ACTOR RUNNER_OS RUNNER_ARCH RUNNER_TEMP RUNNER_TOOL_CACHE XDG_CACHE_HOME COREPACK_HOME PNPM_HOME PNPM_CONFIG_CHILD_CONCURRENCY PNPM_CONFIG_MODULES_DIR PNPM_CONFIG_NETWORK_CONCURRENCY PNPM_CONFIG_STORE_DIR PNPM_CONFIG_VERIFY_DEPS_BEFORE_RUN PNPM_CONFIG_VIRTUAL_STORE_DIR; do
write_export "$key"
done
} > "${env_file}.tmp"
mv "${env_file}.tmp" "$env_file"
{
echo "# Docker containers visible from the hydrated runner"
docker ps --format '{{.Names}}\t{{.Image}}\t{{.Ports}}' 2>/dev/null || true
} > "${services_file}.tmp"
mv "${services_file}.tmp" "$services_file"
tmp="${state}.tmp"
{
echo "WORKSPACE=${GITHUB_WORKSPACE}"
echo "RUN_ID=${GITHUB_RUN_ID}"
echo "JOB=${job}"
echo "ENV_FILE=${env_file}"
echo "SERVICES_FILE=${services_file}"
echo "READY_AT=$(date -u +%Y-%m-%dT%H:%M:%SZ)"
} > "$tmp"
mv "$tmp" "$state"
- name: Keep Crabbox job alive
shell: bash
env:
CRABBOX_ID: ${{ inputs.crabbox_id }}
CRABBOX_KEEP_ALIVE_MINUTES: ${{ inputs.crabbox_keep_alive_minutes }}
run: |
set -euo pipefail
case "$CRABBOX_ID" in
''|*[!A-Za-z0-9._-]*)
echo "Invalid crabbox_id" >&2
exit 2
;;
esac
minutes="${CRABBOX_KEEP_ALIVE_MINUTES}"
case "$minutes" in
''|*[!0-9]*) minutes=90 ;;
esac
stop="$HOME/.crabbox/actions/${CRABBOX_ID}.stop"
deadline=$(( $(date +%s) + minutes * 60 ))
while [ "$(date +%s)" -lt "$deadline" ]; do
if [ -f "$stop" ]; then
exit 0
fi
sleep 15
done
hydrate-github:
name: hydrate-github
if: ${{ inputs.crabbox_job == 'hydrate-github' }}
runs-on: [self-hosted, "${{ inputs.crabbox_runner_label }}"]
timeout-minutes: 120
steps:
- uses: actions/checkout@v6
with:
ref: ${{ inputs.ref || github.ref }}
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
with:
install-bun: "false"
use-actions-cache: "false"
- name: Prepare Crabbox shell
shell: bash
run: |
set -euo pipefail
if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
git fetch --no-tags --depth=50 origin "+refs/heads/main:refs/remotes/origin/main"
fi
node_bin="$(dirname "$(node -p 'process.execPath')")"
sudo ln -sf "$node_bin/node" /usr/local/bin/node
sudo ln -sf "$node_bin/npm" /usr/local/bin/npm
sudo ln -sf "$node_bin/npx" /usr/local/bin/npx
sudo ln -sf "$node_bin/corepack" /usr/local/bin/corepack
sudo tee /usr/local/bin/pnpm >/dev/null <<'PNPM'
#!/usr/bin/env bash
exec /usr/local/bin/corepack pnpm "$@"
PNPM
sudo chmod 0755 /usr/local/bin/pnpm
- name: Ensure Docker is running
shell: bash
run: |
set -euo pipefail
if ! command -v docker >/dev/null 2>&1; then
echo "docker not found; installing fallback engine"
curl -fsSL https://get.docker.com | sudo sh
fi
if command -v systemctl >/dev/null 2>&1; then
sudo systemctl start docker || true
elif command -v service >/dev/null 2>&1; then
sudo service docker start || true
fi
if [ -S /var/run/docker.sock ]; then
sudo usermod -aG docker "$USER" || true
# The runner process keeps its original groups; grant this
# ephemeral runner session access without requiring a relogin.
sudo chmod 666 /var/run/docker.sock
fi
if ! docker buildx version >/dev/null 2>&1; then
arch="$(uname -m)"
case "$arch" in
aarch64|arm64) buildx_arch=arm64 ;;
x86_64|amd64) buildx_arch=amd64 ;;
*) echo "unsupported buildx arch: $arch" >&2; exit 2 ;;
esac
buildx_version="${DOCKER_BUILDX_VERSION:-v0.15.1}"
mkdir -p "$HOME/.docker/cli-plugins"
curl -fsSL \
"https://github.com/docker/buildx/releases/download/${buildx_version}/buildx-${buildx_version}.linux-${buildx_arch}" \
-o "$HOME/.docker/cli-plugins/docker-buildx"
chmod 0755 "$HOME/.docker/cli-plugins/docker-buildx"
fi
docker version
docker buildx version
docker compose version || true
- name: Ensure SSH is available
shell: bash
run: |
set -euo pipefail
if command -v systemctl >/dev/null 2>&1; then
sudo systemctl start ssh || sudo systemctl start sshd || true
elif command -v service >/dev/null 2>&1; then
sudo service ssh start || sudo service sshd start || true
fi
- name: Hydrate provider env helper
shell: bash
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
ANTHROPIC_API_KEY_OLD: ${{ secrets.ANTHROPIC_API_KEY_OLD }}
ANTHROPIC_API_TOKEN: ${{ secrets.ANTHROPIC_API_TOKEN }}
CEREBRAS_API_KEY: ${{ secrets.CEREBRAS_API_KEY }}
DEEPINFRA_API_KEY: ${{ secrets.DEEPINFRA_API_KEY }}
FACTORY_API_KEY: ${{ secrets.FACTORY_API_KEY }}
FIREWORKS_API_KEY: ${{ secrets.FIREWORKS_API_KEY }}
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}
KIMI_API_KEY: ${{ secrets.KIMI_API_KEY }}
MINIMAX_API_KEY: ${{ secrets.MINIMAX_API_KEY }}
MISTRAL_API_KEY: ${{ secrets.MISTRAL_API_KEY }}
MOONSHOT_API_KEY: ${{ secrets.MOONSHOT_API_KEY }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL }}
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
QWEN_API_KEY: ${{ secrets.QWEN_API_KEY }}
TOGETHER_API_KEY: ${{ secrets.TOGETHER_API_KEY }}
XAI_API_KEY: ${{ secrets.XAI_API_KEY }}
ZAI_API_KEY: ${{ secrets.ZAI_API_KEY }}
Z_AI_API_KEY: ${{ secrets.Z_AI_API_KEY }}
run: bash scripts/ci-hydrate-testbox-env.sh
- name: Mark Crabbox ready
shell: bash
env:
CRABBOX_ID: ${{ inputs.crabbox_id }}
CRABBOX_JOB: ${{ inputs.crabbox_job }}
run: |
set -euo pipefail
job="${CRABBOX_JOB}"
if [ -z "$job" ]; then job=hydrate-github; fi
case "$CRABBOX_ID" in
''|*[!A-Za-z0-9._-]*)
echo "Invalid crabbox_id" >&2
exit 2
;;
esac
mkdir -p "$HOME/.crabbox/actions"
state="$HOME/.crabbox/actions/${CRABBOX_ID}.env"
env_file="$HOME/.crabbox/actions/${CRABBOX_ID}.env.sh"
services_file="$HOME/.crabbox/actions/${CRABBOX_ID}.services"
write_export() {
key="$1"
value="${!key-}"
if [ -n "$value" ]; then
printf 'export %s=%q\n' "$key" "$value"
fi
}
{
for key in CI GITHUB_ACTIONS GITHUB_WORKSPACE GITHUB_REPOSITORY GITHUB_RUN_ID GITHUB_RUN_NUMBER GITHUB_RUN_ATTEMPT GITHUB_REF GITHUB_REF_NAME GITHUB_SHA GITHUB_EVENT_NAME GITHUB_ACTOR RUNNER_OS RUNNER_ARCH RUNNER_TEMP RUNNER_TOOL_CACHE PNPM_CONFIG_CHILD_CONCURRENCY PNPM_CONFIG_MODULES_DIR PNPM_CONFIG_NETWORK_CONCURRENCY PNPM_CONFIG_STORE_DIR PNPM_CONFIG_VERIFY_DEPS_BEFORE_RUN PNPM_CONFIG_VIRTUAL_STORE_DIR; do
write_export "$key"
done
} > "${env_file}.tmp"

View File

@@ -34,11 +34,15 @@ jobs:
const isDependencyFile = (filename) =>
filename === "package.json" ||
filename === "package-lock.json" ||
filename === "npm-shrinkwrap.json" ||
filename === "pnpm-lock.yaml" ||
filename === "pnpm-workspace.yaml" ||
filename === "ui/package.json" ||
filename.startsWith("patches/") ||
/^packages\/[^/]+\/package\.json$/u.test(filename) ||
/^extensions\/[^/]+\/package-lock\.json$/u.test(filename) ||
/^extensions\/[^/]+\/npm-shrinkwrap\.json$/u.test(filename) ||
/^extensions\/[^/]+\/package\.json$/u.test(filename);
const sanitizeDisplayValue = (value) =>
@@ -143,7 +147,8 @@ jobs:
"",
"Maintainer follow-up:",
"- Review whether the dependency changes are intentional.",
"- Inspect resolved package deltas when lockfile or workspace dependency policy changes are present.",
"- Inspect resolved package deltas when lockfile, shrinkwrap, or workspace dependency policy changes are present.",
"- Treat `package-lock.json` and `npm-shrinkwrap.json` diffs as security-review surfaces.",
"- Run `pnpm deps:changes:report -- --base-ref origin/main --markdown /tmp/dependency-changes.md --json /tmp/dependency-changes.json` locally for detailed release-style evidence.",
].join("\n");

View File

@@ -119,7 +119,6 @@ env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
GH_REPO: ${{ github.repository }}
NODE_VERSION: "24.15.0"
PNPM_VERSION: "11.0.8"
jobs:
resolve_target:
@@ -135,7 +134,7 @@ jobs:
ref: ${{ github.ref_name }}
path: workflow
fetch-depth: 1
persist-credentials: false
persist-credentials: true
submodules: false
- name: Resolve target SHA
@@ -233,7 +232,7 @@ jobs:
with:
ref: ${{ needs.resolve_target.outputs.sha }}
fetch-depth: 1
persist-credentials: false
persist-credentials: true
- name: Verify Docker runtime-assets prune path
env:
@@ -242,7 +241,7 @@ jobs:
set -euo pipefail
timeout --foreground --kill-after=30s 35m docker build \
--target runtime-assets \
--build-arg OPENCLAW_EXTENSIONS="matrix" \
--build-arg OPENCLAW_EXTENSIONS="diagnostics-otel,codex" \
.
normal_ci:
@@ -271,9 +270,31 @@ jobs:
shift
local before_json dispatch_output run_id status conclusion url poll_count
before_json="$(gh run list --workflow "$workflow" --event workflow_dispatch --limit 100 --json databaseId --jq '[.[].databaseId]')"
gh_with_retry() {
local output status attempt
for attempt in 1 2 3 4 5 6; do
set +e
output="$(gh "$@" 2>&1)"
status=$?
set -e
if [[ "$status" -eq 0 ]]; then
printf '%s\n' "$output"
return 0
fi
if [[ "$output" == *"Bad credentials"* || "$output" == *"HTTP 401"* || "$output" == *"secondary rate limit"* || "$output" == *"API rate limit"* ]]; then
echo "::warning::gh $* failed on attempt ${attempt}: ${output}" >&2
sleep $((attempt * 10))
continue
fi
printf '%s\n' "$output" >&2
return "$status"
done
printf '%s\n' "$output" >&2
return "$status"
}
before_json="$(gh_with_retry run list --workflow "$workflow" --event workflow_dispatch --limit 100 --json databaseId --jq '[.[].databaseId]')"
dispatch_output="$(gh workflow run "$workflow" --ref "$CHILD_WORKFLOW_REF" "$@" 2>&1)"
dispatch_output="$(gh_with_retry workflow run "$workflow" --ref "$CHILD_WORKFLOW_REF" "$@")"
printf '%s\n' "$dispatch_output"
run_id="$(
printf '%s\n' "$dispatch_output" |
@@ -284,7 +305,7 @@ jobs:
if [[ -z "$run_id" ]]; then
for _ in $(seq 1 60); do
run_id="$(
BEFORE_IDS="$before_json" gh run list --workflow "$workflow" --event workflow_dispatch --limit 50 --json databaseId,createdAt \
BEFORE_IDS="$before_json" gh_with_retry run list --workflow "$workflow" --event workflow_dispatch --limit 50 --json databaseId,createdAt \
--jq 'map(select(.databaseId as $id | (env.BEFORE_IDS | fromjson | index($id) | not))) | sort_by(.createdAt) | reverse | .[0].databaseId // empty'
)"
if [[ -n "$run_id" ]]; then
@@ -302,6 +323,14 @@ jobs:
echo "Dispatched ${workflow}: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${run_id}"
echo "run_id=${run_id}" >> "$GITHUB_OUTPUT"
fetch_child_run_json() {
gh_with_retry api "repos/${GITHUB_REPOSITORY}/actions/runs/${run_id}"
}
fetch_child_jobs() {
gh_with_retry api --paginate "repos/${GITHUB_REPOSITORY}/actions/runs/${run_id}/jobs?per_page=100" --jq '.jobs[]'
}
cancel_child() {
if [[ -n "${run_id:-}" ]]; then
echo "Cancelling child workflow ${workflow}: ${run_id}" >&2
@@ -312,26 +341,26 @@ jobs:
poll_count=0
while true; do
status="$(gh run view "$run_id" --json status --jq '.status')"
status="$(fetch_child_run_json | jq -r '.status')"
if [[ "$status" == "completed" ]]; then
break
fi
poll_count=$((poll_count + 1))
if (( poll_count % 10 == 0 )); then
echo "Still waiting on ${workflow}: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${run_id}"
gh run view "$run_id" --json jobs --jq '.jobs[] | select(.status != "completed") | {name, status, url}' || true
fetch_child_jobs | jq 'select(.status != "completed") | {name, status, url: .html_url}' || true
fi
sleep 30
done
trap - EXIT INT TERM
conclusion="$(gh run view "$run_id" --json conclusion --jq '.conclusion')"
url="$(gh run view "$run_id" --json url --jq '.url')"
conclusion="$(fetch_child_run_json | jq -r '.conclusion // ""')"
url="$(fetch_child_run_json | jq -r '.html_url')"
echo "${workflow} finished with ${conclusion}: ${url}"
echo "url=${url}" >> "$GITHUB_OUTPUT"
echo "conclusion=${conclusion}" >> "$GITHUB_OUTPUT"
if [[ "$conclusion" != "success" ]]; then
gh run view "$run_id" --json jobs --jq '.jobs[] | select(.conclusion != "success" and .conclusion != "skipped") | {name, conclusion, url}' || true
fetch_child_jobs | jq 'select(.conclusion != "success" and .conclusion != "skipped") | {name, conclusion, url: .html_url}' || true
exit 1
fi
}
@@ -371,9 +400,31 @@ jobs:
shift
local before_json dispatch_output run_id status conclusion url poll_count
before_json="$(gh run list --workflow "$workflow" --event workflow_dispatch --limit 100 --json databaseId --jq '[.[].databaseId]')"
gh_with_retry() {
local output status attempt
for attempt in 1 2 3 4 5 6; do
set +e
output="$(gh "$@" 2>&1)"
status=$?
set -e
if [[ "$status" -eq 0 ]]; then
printf '%s\n' "$output"
return 0
fi
if [[ "$output" == *"Bad credentials"* || "$output" == *"HTTP 401"* || "$output" == *"secondary rate limit"* || "$output" == *"API rate limit"* ]]; then
echo "::warning::gh $* failed on attempt ${attempt}: ${output}" >&2
sleep $((attempt * 10))
continue
fi
printf '%s\n' "$output" >&2
return "$status"
done
printf '%s\n' "$output" >&2
return "$status"
}
before_json="$(gh_with_retry run list --workflow "$workflow" --event workflow_dispatch --limit 100 --json databaseId --jq '[.[].databaseId]')"
dispatch_output="$(gh workflow run "$workflow" --ref "$CHILD_WORKFLOW_REF" "$@" 2>&1)"
dispatch_output="$(gh_with_retry workflow run "$workflow" --ref "$CHILD_WORKFLOW_REF" "$@")"
printf '%s\n' "$dispatch_output"
run_id="$(
printf '%s\n' "$dispatch_output" |
@@ -384,7 +435,7 @@ jobs:
if [[ -z "$run_id" ]]; then
for _ in $(seq 1 60); do
run_id="$(
BEFORE_IDS="$before_json" gh run list --workflow "$workflow" --event workflow_dispatch --limit 50 --json databaseId,createdAt \
BEFORE_IDS="$before_json" gh_with_retry run list --workflow "$workflow" --event workflow_dispatch --limit 50 --json databaseId,createdAt \
--jq 'map(select(.databaseId as $id | (env.BEFORE_IDS | fromjson | index($id) | not))) | sort_by(.createdAt) | reverse | .[0].databaseId // empty'
)"
if [[ -n "$run_id" ]]; then
@@ -402,6 +453,14 @@ jobs:
echo "Dispatched ${workflow}: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${run_id}"
echo "run_id=${run_id}" >> "$GITHUB_OUTPUT"
fetch_child_run_json() {
gh_with_retry api "repos/${GITHUB_REPOSITORY}/actions/runs/${run_id}"
}
fetch_child_jobs() {
gh_with_retry api --paginate "repos/${GITHUB_REPOSITORY}/actions/runs/${run_id}/jobs?per_page=100" --jq '.jobs[]'
}
cancel_child() {
if [[ -n "${run_id:-}" ]]; then
echo "Cancelling child workflow ${workflow}: ${run_id}" >&2
@@ -412,26 +471,26 @@ jobs:
poll_count=0
while true; do
status="$(gh run view "$run_id" --json status --jq '.status')"
status="$(fetch_child_run_json | jq -r '.status')"
if [[ "$status" == "completed" ]]; then
break
fi
poll_count=$((poll_count + 1))
if (( poll_count % 10 == 0 )); then
echo "Still waiting on ${workflow}: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${run_id}"
gh run view "$run_id" --json jobs --jq '.jobs[] | select(.status != "completed") | {name, status, url}' || true
fetch_child_jobs | jq 'select(.status != "completed") | {name, status, url: .html_url}' || true
fi
sleep 30
done
trap - EXIT INT TERM
conclusion="$(gh run view "$run_id" --json conclusion --jq '.conclusion')"
url="$(gh run view "$run_id" --json url --jq '.url')"
conclusion="$(fetch_child_run_json | jq -r '.conclusion // ""')"
url="$(fetch_child_run_json | jq -r '.html_url')"
echo "${workflow} finished with ${conclusion}: ${url}"
echo "url=${url}" >> "$GITHUB_OUTPUT"
echo "conclusion=${conclusion}" >> "$GITHUB_OUTPUT"
if [[ "$conclusion" != "success" ]]; then
gh run view "$run_id" --json jobs --jq '.jobs[] | select(.conclusion != "success" and .conclusion != "skipped") | {name, conclusion, url}' || true
fetch_child_jobs | jq 'select(.conclusion != "success" and .conclusion != "skipped") | {name, conclusion, url: .html_url}' || true
exit 1
fi
}
@@ -480,10 +539,32 @@ jobs:
local workflow="$1"
shift
local before_json dispatch_output run_id status conclusion url poll_count
before_json="$(gh run list --workflow "$workflow" --event workflow_dispatch --limit 100 --json databaseId --jq '[.[].databaseId]')"
local before_json dispatch_output run_id status conclusion url poll_count run_json
gh_with_retry() {
local output status attempt
for attempt in 1 2 3 4 5 6; do
set +e
output="$(gh "$@" 2>&1)"
status=$?
set -e
if [[ "$status" -eq 0 ]]; then
printf '%s\n' "$output"
return 0
fi
if [[ "$output" == *"Bad credentials"* || "$output" == *"HTTP 401"* || "$output" == *"secondary rate limit"* || "$output" == *"API rate limit"* ]]; then
echo "::warning::gh $* failed on attempt ${attempt}: ${output}" >&2
sleep $((attempt * 10))
continue
fi
printf '%s\n' "$output" >&2
return "$status"
done
printf '%s\n' "$output" >&2
return "$status"
}
before_json="$(gh_with_retry run list --workflow "$workflow" --event workflow_dispatch --limit 100 --json databaseId --jq '[.[].databaseId]')"
dispatch_output="$(gh workflow run "$workflow" --ref "$CHILD_WORKFLOW_REF" "$@" 2>&1)"
dispatch_output="$(gh_with_retry workflow run "$workflow" --ref "$CHILD_WORKFLOW_REF" "$@")"
printf '%s\n' "$dispatch_output"
run_id="$(
printf '%s\n' "$dispatch_output" |
@@ -494,7 +575,7 @@ jobs:
if [[ -z "$run_id" ]]; then
for _ in $(seq 1 60); do
run_id="$(
BEFORE_IDS="$before_json" gh run list --workflow "$workflow" --event workflow_dispatch --limit 50 --json databaseId,createdAt \
BEFORE_IDS="$before_json" gh_with_retry run list --workflow "$workflow" --event workflow_dispatch --limit 50 --json databaseId,createdAt \
--jq 'map(select(.databaseId as $id | (env.BEFORE_IDS | fromjson | index($id) | not))) | sort_by(.createdAt) | reverse | .[0].databaseId // empty'
)"
if [[ -n "$run_id" ]]; then
@@ -512,6 +593,54 @@ jobs:
echo "Dispatched ${workflow}: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${run_id}"
echo "run_id=${run_id}" >> "$GITHUB_OUTPUT"
fetch_child_run_json() {
gh_with_retry api "repos/${GITHUB_REPOSITORY}/actions/runs/${run_id}"
}
fetch_child_jobs() {
gh_with_retry api --paginate "repos/${GITHUB_REPOSITORY}/actions/runs/${run_id}/jobs?per_page=100" --jq '.jobs[]'
}
release_check_blocking_job() {
case "$1" in
"resolve_target" | \
"Prepare release package artifact" | \
"install_smoke_release_checks / "* | \
"Run package acceptance" | \
"Run package acceptance / "*)
return 0
;;
esac
return 1
}
release_checks_advisory_only() {
local run_json="$1"
local verifier_conclusion name saw_advisory failed
verifier_conclusion="$(
jq -r '.jobs[] | select(.name == "Verify release checks") | .conclusion' <<< "$run_json" |
tail -n 1
)"
if [[ "$verifier_conclusion" != "success" ]]; then
return 1
fi
saw_advisory=0
failed=0
while IFS= read -r name; do
[[ -z "${name// }" ]] && continue
if release_check_blocking_job "$name"; then
echo "::error::${name} is a package-safety Tideclaw alpha release-check lane."
failed=1
else
saw_advisory=1
fi
done < <(jq -r '.jobs[] | select(.conclusion != "success" and .conclusion != "skipped") | .name' <<< "$run_json")
[[ "$saw_advisory" == "1" && "$failed" == "0" ]]
}
cancel_child() {
if [[ -n "${run_id:-}" ]]; then
echo "Cancelling child workflow ${workflow}: ${run_id}" >&2
@@ -522,26 +651,38 @@ jobs:
poll_count=0
while true; do
status="$(gh run view "$run_id" --json status --jq '.status')"
status="$(fetch_child_run_json | jq -r '.status')"
if [[ "$status" == "completed" ]]; then
break
fi
poll_count=$((poll_count + 1))
if (( poll_count % 10 == 0 )); then
echo "Still waiting on ${workflow}: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${run_id}"
gh run view "$run_id" --json jobs --jq '.jobs[] | select(.status != "completed") | {name, status, url}' || true
fetch_child_jobs | jq 'select(.status != "completed") | {name, status, url: .html_url}' || true
fi
sleep 30
done
trap - EXIT INT TERM
conclusion="$(gh run view "$run_id" --json conclusion --jq '.conclusion')"
url="$(gh run view "$run_id" --json url --jq '.url')"
jobs_json="$(fetch_child_jobs | jq -s '{jobs: [.[] | {name, conclusion, url: .html_url}]}')"
run_json="$(
jq -s '.[0] + .[1]' \
<(fetch_child_run_json | jq '{conclusion: (.conclusion // ""), url: .html_url}') \
<(printf '%s\n' "$jobs_json")
)"
conclusion="$(jq -r '.conclusion' <<< "$run_json")"
url="$(jq -r '.url' <<< "$run_json")"
echo "${workflow} finished with ${conclusion}: ${url}"
echo "url=${url}" >> "$GITHUB_OUTPUT"
echo "conclusion=${conclusion}" >> "$GITHUB_OUTPUT"
if [[ "$conclusion" != "success" ]]; then
gh run view "$run_id" --json jobs --jq '.jobs[] | select(.conclusion != "success" and .conclusion != "skipped") | {name, conclusion, url}' || true
jq '.jobs[] | select(.conclusion != "success" and .conclusion != "skipped") | {name, conclusion, url}' <<< "$run_json" || true
if [[ "$workflow" == "openclaw-release-checks.yml" && "$CHILD_WORKFLOW_REF" =~ ^tideclaw/alpha/[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{4}Z$ ]]; then
if release_checks_advisory_only "$run_json"; then
echo "::warning::${workflow} ended with ${conclusion}, but Verify release checks accepted Tideclaw alpha advisory lanes."
return 0
fi
fi
exit 1
fi
}
@@ -623,7 +764,7 @@ jobs:
- name: Checkout trusted workflow ref
uses: actions/checkout@v6
with:
persist-credentials: false
persist-credentials: true
ref: ${{ github.ref_name }}
fetch-depth: 0
@@ -635,7 +776,6 @@ jobs:
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
install-bun: "true"
install-deps: "false"
@@ -702,7 +842,30 @@ jobs:
run: |
set -euo pipefail
before_json="$(gh run list --workflow npm-telegram-beta-e2e.yml --event workflow_dispatch --limit 100 --json databaseId --jq '[.[].databaseId]')"
gh_with_retry() {
local output status attempt
for attempt in 1 2 3 4 5 6; do
set +e
output="$(gh "$@" 2>&1)"
status=$?
set -e
if [[ "$status" -eq 0 ]]; then
printf '%s\n' "$output"
return 0
fi
if [[ "$output" == *"Bad credentials"* || "$output" == *"HTTP 401"* || "$output" == *"secondary rate limit"* || "$output" == *"API rate limit"* ]]; then
echo "::warning::gh $* failed on attempt ${attempt}: ${output}" >&2
sleep $((attempt * 10))
continue
fi
printf '%s\n' "$output" >&2
return "$status"
done
printf '%s\n' "$output" >&2
return "$status"
}
before_json="$(gh_with_retry run list --workflow npm-telegram-beta-e2e.yml --event workflow_dispatch --limit 100 --json databaseId --jq '[.[].databaseId]')"
args=(-f package_spec="${PACKAGE_SPEC:-openclaw@beta}" -f harness_ref="$TARGET_SHA" -f provider_mode="$PROVIDER_MODE")
if [[ -z "${PACKAGE_SPEC// }" ]]; then
@@ -720,12 +883,12 @@ jobs:
args+=(-f scenario="$SCENARIO")
fi
gh workflow run npm-telegram-beta-e2e.yml --ref "$CHILD_WORKFLOW_REF" "${args[@]}"
gh_with_retry workflow run npm-telegram-beta-e2e.yml --ref "$CHILD_WORKFLOW_REF" "${args[@]}"
run_id=""
for _ in $(seq 1 60); do
run_id="$(
BEFORE_IDS="$before_json" gh run list --workflow npm-telegram-beta-e2e.yml --event workflow_dispatch --limit 50 --json databaseId,createdAt \
BEFORE_IDS="$before_json" gh_with_retry run list --workflow npm-telegram-beta-e2e.yml --event workflow_dispatch --limit 50 --json databaseId,createdAt \
--jq 'map(select(.databaseId as $id | (env.BEFORE_IDS | fromjson | index($id) | not))) | sort_by(.createdAt) | reverse | .[0].databaseId // empty'
)"
if [[ -n "$run_id" ]]; then
@@ -752,26 +915,26 @@ jobs:
poll_count=0
while true; do
status="$(gh run view "$run_id" --json status --jq '.status')"
status="$(gh_with_retry run view "$run_id" --json status --jq '.status')"
if [[ "$status" == "completed" ]]; then
break
fi
poll_count=$((poll_count + 1))
if (( poll_count % 10 == 0 )); then
echo "Still waiting on npm-telegram-beta-e2e.yml: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${run_id}"
gh run view "$run_id" --json jobs --jq '.jobs[] | select(.status != "completed") | {name, status, url}' || true
gh_with_retry run view "$run_id" --json jobs --jq '.jobs[] | select(.status != "completed") | {name, status, url}' || true
fi
sleep 30
done
trap - EXIT INT TERM
conclusion="$(gh run view "$run_id" --json conclusion --jq '.conclusion')"
url="$(gh run view "$run_id" --json url --jq '.url')"
conclusion="$(gh_with_retry run view "$run_id" --json conclusion --jq '.conclusion')"
url="$(gh_with_retry run view "$run_id" --json url --jq '.url')"
echo "npm-telegram-beta-e2e.yml finished with ${conclusion}: ${url}"
echo "url=${url}" >> "$GITHUB_OUTPUT"
echo "conclusion=${conclusion}" >> "$GITHUB_OUTPUT"
if [[ "$conclusion" != "success" ]]; then
gh run view "$run_id" --json jobs --jq '.jobs[] | select(.conclusion != "success" and .conclusion != "skipped") | {name, conclusion, url}' || true
gh_with_retry run view "$run_id" --json jobs --jq '.jobs[] | select(.conclusion != "success" and .conclusion != "skipped") | {name, conclusion, url}' || true
exit 1
fi
@@ -800,10 +963,51 @@ jobs:
run: |
set -euo pipefail
release_check_blocking_job() {
case "$1" in
"resolve_target" | \
"Prepare release package artifact" | \
"install_smoke_release_checks / "* | \
"Run package acceptance" | \
"Run package acceptance / "*)
return 0
;;
esac
return 1
}
release_checks_advisory_only() {
local run_json="$1"
local verifier_conclusion name saw_advisory failed
verifier_conclusion="$(
jq -r '.jobs[] | select(.name == "Verify release checks") | .conclusion' <<< "$run_json" |
tail -n 1
)"
if [[ "$verifier_conclusion" != "success" ]]; then
return 1
fi
saw_advisory=0
failed=0
while IFS= read -r name; do
[[ -z "${name// }" ]] && continue
if release_check_blocking_job "$name"; then
echo "::error::${name} is a package-safety Tideclaw alpha release-check lane."
failed=1
else
saw_advisory=1
fi
done < <(jq -r '.jobs[] | select(.conclusion != "success" and .conclusion != "skipped") | .name' <<< "$run_json")
[[ "$saw_advisory" == "1" && "$failed" == "0" ]]
}
check_child() {
local label="$1"
local run_id="$2"
local required="$3"
local advisory_ok="${4:-0}"
if [[ -z "${run_id// }" ]]; then
if [[ "$required" == "0" ]]; then
@@ -829,6 +1033,12 @@ jobs:
fi
if [[ "$status" != "completed" || "$conclusion" != "success" ]]; then
if [[ "$advisory_ok" == "1" && "$label" == "release_checks" ]]; then
if release_checks_advisory_only "$run_json"; then
echo "::warning::${label} child run ended with ${status}/${conclusion}, but Verify release checks accepted Tideclaw alpha advisory lanes: ${url}"
return 0
fi
fi
echo "::error::${label} child run ended with ${status}/${conclusion}: ${url}"
jq '.jobs[] | select(.conclusion != "success" and .conclusion != "skipped") | {name, status, conclusion, url}' <<< "$run_json" || true
return 1
@@ -1018,6 +1228,8 @@ jobs:
if [[ "$RELEASE_CHECKS_RESULT" == "skipped" && -z "${RELEASE_CHECKS_RUN_ID// }" ]]; then
check_child "release_checks" "" "$release_checks_required" || failed=1
elif [[ "$CHILD_WORKFLOW_REF" =~ ^tideclaw/alpha/[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{4}Z$ ]]; then
check_child "release_checks" "$RELEASE_CHECKS_RUN_ID" 1 1 || failed=1
else
check_child "release_checks" "$RELEASE_CHECKS_RUN_ID" 1 || failed=1
fi

View File

@@ -109,6 +109,7 @@ jobs:
uses: actions/checkout@v6
with:
ref: ${{ inputs.ref || github.ref }}
persist-credentials: false
- name: Set up Blacksmith Docker Builder
uses: useblacksmith/setup-docker-builder@722e97d12b1d06a961800dd6c05d79d951ad3c80 # v1
@@ -219,6 +220,7 @@ jobs:
uses: actions/checkout@v6
with:
ref: ${{ inputs.ref || github.ref }}
persist-credentials: false
- name: Log in to GHCR
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
@@ -290,6 +292,7 @@ jobs:
uses: actions/checkout@v6
with:
ref: ${{ inputs.ref || github.ref }}
persist-credentials: false
- name: Run QR package install smoke
env:
@@ -305,6 +308,7 @@ jobs:
uses: actions/checkout@v6
with:
ref: ${{ inputs.ref || github.ref }}
persist-credentials: false
- name: Log in to GHCR
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
@@ -410,6 +414,7 @@ jobs:
uses: actions/checkout@v6
with:
ref: ${{ inputs.ref || github.ref }}
persist-credentials: false
- name: Log in to GHCR
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
@@ -454,10 +459,10 @@ jobs:
- name: Run installer docker tests
env:
OPENCLAW_INSTALL_URL: https://openclaw.ai/install.sh
OPENCLAW_INSTALL_CLI_URL: https://openclaw.ai/install-cli.sh
OPENCLAW_INSTALL_URL: file:///tmp/openclaw-install.sh
OPENCLAW_INSTALL_CLI_URL: file:///tmp/openclaw-install-cli.sh
OPENCLAW_NO_ONBOARD: "1"
OPENCLAW_INSTALL_SMOKE_SKIP_CLI: "1"
OPENCLAW_INSTALL_SMOKE_SKIP_CLI: "0"
OPENCLAW_INSTALL_SMOKE_SKIP_IMAGE_BUILD: "1"
OPENCLAW_INSTALL_NONROOT_SKIP_IMAGE_BUILD: "1"
OPENCLAW_INSTALL_SMOKE_SKIP_NONROOT: "0"
@@ -468,6 +473,15 @@ jobs:
OPENCLAW_INSTALL_SMOKE_UPDATE_SKIP_LOCAL_BUILD: "1"
run: bash scripts/test-install-sh-docker.sh
- name: Run Rocky Linux installer smoke
run: |
timeout 20m docker run --rm \
-e OPENCLAW_NO_ONBOARD=1 \
-e OPENCLAW_NO_PROMPT=1 \
-v "$PWD/scripts/install.sh:/tmp/install.sh:ro" \
rockylinux:9@sha256:d7be1c094cc5845ee815d4632fe377514ee6ebcf8efaed6892889657e5ddaaa6 \
bash -lc 'dnf install -y -q ca-certificates tar gzip xz findutils which sudo >/dev/null && bash /tmp/install.sh --install-method npm --version latest --no-onboard --no-prompt --verify && openclaw --version'
bun_global_install_smoke:
needs: [preflight, root_dockerfile_image]
if: needs.preflight.outputs.run_full_install_smoke == 'true' && needs.preflight.outputs.run_bun_global_install_smoke == 'true'
@@ -477,6 +491,7 @@ jobs:
uses: actions/checkout@v6
with:
ref: ${{ inputs.ref || github.ref }}
persist-credentials: false
- name: Log in to GHCR
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
@@ -515,6 +530,7 @@ jobs:
uses: actions/checkout@v6
with:
ref: ${{ inputs.ref || github.ref }}
persist-credentials: false
- name: Set up Blacksmith Docker Builder
uses: useblacksmith/setup-docker-builder@722e97d12b1d06a961800dd6c05d79d951ad3c80 # v1

View File

@@ -89,10 +89,10 @@ jobs:
per_page: 100,
});
const excludedLockfiles = new Set(["pnpm-lock.yaml", "package-lock.json", "yarn.lock", "bun.lockb"]);
const excludedLockfiles = new Set(["pnpm-lock.yaml", "package-lock.json", "npm-shrinkwrap.json", "yarn.lock", "bun.lockb"]);
const totalChangedLines = files.reduce((total, file) => {
const path = file.filename ?? "";
if (path.startsWith("docs/") || excludedLockfiles.has(path)) {
if (path.startsWith("docs/") || excludedLockfiles.has(path) || path.endsWith("/package-lock.json") || path.endsWith("/npm-shrinkwrap.json")) {
return total;
}
return total + (file.additions ?? 0) + (file.deletions ?? 0);
@@ -603,10 +603,10 @@ jobs:
per_page: 100,
});
const excludedLockfiles = new Set(["pnpm-lock.yaml", "package-lock.json", "yarn.lock", "bun.lockb"]);
const excludedLockfiles = new Set(["pnpm-lock.yaml", "package-lock.json", "npm-shrinkwrap.json", "yarn.lock", "bun.lockb"]);
const totalChangedLines = files.reduce((total, file) => {
const path = file.filename ?? "";
if (path.startsWith("docs/") || excludedLockfiles.has(path)) {
if (path.startsWith("docs/") || excludedLockfiles.has(path) || path.endsWith("/package-lock.json") || path.endsWith("/npm-shrinkwrap.json")) {
return total;
}
return total + (file.additions ?? 0) + (file.deletions ?? 0);

View File

@@ -25,7 +25,6 @@ concurrency:
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
NODE_VERSION: "24.15.0"
PNPM_VERSION: "11.0.8"
jobs:
validate_macos_release_request:
@@ -53,7 +52,6 @@ jobs:
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
install-bun: "false"
- name: Ensure matching GitHub release exists

View File

@@ -25,7 +25,6 @@ concurrency:
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
NODE_VERSION: "24.x"
PNPM_VERSION: "11.0.8"
OPENCLAW_BUILD_PRIVATE_QA: "1"
OPENCLAW_ENABLE_PRIVATE_QA_CLI: "1"
@@ -142,7 +141,6 @@ jobs:
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
install-bun: "true"
- name: Build private QA runtime

View File

@@ -32,7 +32,6 @@ concurrency:
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
NODE_VERSION: "24.x"
PNPM_VERSION: "11.0.8"
OPENCLAW_BUILD_PRIVATE_QA: "1"
OPENCLAW_ENABLE_PRIVATE_QA_CLI: "1"
@@ -255,7 +254,6 @@ jobs:
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
install-bun: "true"
- name: Build Mantis harness

View File

@@ -32,7 +32,6 @@ concurrency:
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
NODE_VERSION: "24.x"
PNPM_VERSION: "11.0.8"
OPENCLAW_BUILD_PRIVATE_QA: "1"
OPENCLAW_ENABLE_PRIVATE_QA_CLI: "1"
@@ -245,7 +244,6 @@ jobs:
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
install-bun: "true"
- name: Build Mantis harness

View File

@@ -17,6 +17,11 @@ on:
required: true
default: slack-canary
type: string
approval_checkpoints:
description: Run native Slack approval checkpoint mode instead of gateway setup
required: false
default: false
type: boolean
keep_vm:
description: Keep the desktop lease open after a passing run
required: false
@@ -30,6 +35,14 @@ on:
options:
- aws
- hetzner
crabbox_market:
description: Crabbox capacity market for AWS leases
required: false
default: on-demand
type: choice
options:
- on-demand
- spot
crabbox_lease_id:
description: Optional existing Crabbox desktop/browser lease id or slug to reuse
required: false
@@ -55,7 +68,6 @@ concurrency:
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
NODE_VERSION: "24.x"
PNPM_VERSION: "11.0.8"
OPENCLAW_BUILD_PRIVATE_QA: "1"
OPENCLAW_ENABLE_PRIVATE_QA_CLI: "1"
CRABBOX_REF: main
@@ -162,7 +174,6 @@ jobs:
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
install-bun: "true"
- name: Build Mantis harness
@@ -229,9 +240,11 @@ jobs:
CRABBOX_ACCESS_CLIENT_SECRET: ${{ secrets.CRABBOX_ACCESS_CLIENT_SECRET }}
CRABBOX_LEASE_ID: ${{ inputs.crabbox_lease_id }}
CRABBOX_PROVIDER: ${{ inputs.crabbox_provider }}
CRABBOX_MARKET: ${{ inputs.crabbox_market }}
KEEP_VM: ${{ inputs.keep_vm }}
HYDRATE_MODE: ${{ inputs.hydrate_mode }}
SCENARIO_ID: ${{ inputs.scenario_id }}
APPROVAL_CHECKPOINTS: ${{ inputs.approval_checkpoints }}
shell: bash
run: |
set -euo pipefail
@@ -252,6 +265,15 @@ jobs:
require_var OPENCLAW_QA_CONVEX_SITE_URL
require_var OPENCLAW_QA_CONVEX_SECRET_CI
require_var CRABBOX_COORDINATOR_TOKEN
if [[ -z "${CRABBOX_LEASE_ID:-}" && "$CRABBOX_PROVIDER" == "aws" ]]; then
runner_ip="$(curl -fsS https://checkip.amazonaws.com | tr -d '[:space:]')"
if [[ -z "$runner_ip" ]]; then
echo "Could not resolve GitHub runner public IPv4 for AWS SSH ingress." >&2
exit 1
fi
export CRABBOX_AWS_SSH_CIDRS="${runner_ip}/32"
echo "Using AWS SSH CIDR ${CRABBOX_AWS_SSH_CIDRS}"
fi
candidate_repo="$(pwd)/.artifacts/qa-e2e/mantis/slack-desktop-smoke-worktrees/candidate"
output_rel=".artifacts/qa-e2e/mantis/slack-desktop-smoke"
@@ -267,6 +289,22 @@ jobs:
else
keep_args=(--no-keep-lease)
fi
market_args=()
if [[ -n "${CRABBOX_MARKET:-}" ]]; then
market_args=(--market "$CRABBOX_MARKET")
fi
gateway_args=(--gateway-setup)
approval_args=()
scenario_args=(--scenario "$SCENARIO_ID")
scenario_label="$SCENARIO_ID"
if [[ "$APPROVAL_CHECKPOINTS" == "true" ]]; then
approval_args=(--approval-checkpoints)
gateway_args=()
if [[ -z "${SCENARIO_ID:-}" || "$SCENARIO_ID" == "slack-canary" || "$SCENARIO_ID" == "approval-checkpoints" ]]; then
scenario_args=()
scenario_label="approval-checkpoints"
fi
fi
set +e
pnpm openclaw qa mantis slack-desktop-smoke \
@@ -276,7 +314,7 @@ jobs:
--class standard \
--idle-timeout 45m \
--ttl 120m \
--gateway-setup \
"${gateway_args[@]}" \
--credential-source convex \
--credential-role ci \
--provider-mode live-frontier \
@@ -284,7 +322,9 @@ jobs:
--model openai/gpt-5.5 \
--alt-model openai/gpt-5.5 \
--fast \
--scenario "$SCENARIO_ID" \
"${scenario_args[@]}" \
"${approval_args[@]}" \
"${market_args[@]}" \
"${keep_args[@]}" \
"${lease_args[@]}"
mantis_exit=$?
@@ -314,27 +354,81 @@ jobs:
status="$(jq -r '.status' "$root/mantis-slack-desktop-smoke-summary.json")"
screenshot_required=false
desktop_capture_inline=true
if [[ "$status" == "pass" ]]; then
screenshot_required=true
fi
evidence_summary="Mantis ran Slack QA inside a Crabbox Linux VNC desktop, started an OpenClaw Slack gateway in that VM, opened Slack Web in the visible browser, and captured screenshot/video evidence."
expected_result="Slack QA and VM gateway setup pass"
checkpoint_artifacts='[]'
checkpoint_required=false
if [[ "$APPROVAL_CHECKPOINTS" == "true" ]]; then
evidence_summary="Mantis ran Slack native approval QA inside a Crabbox Linux VNC desktop, rendered pending/resolved approval checkpoints from the Slack API messages, and stored Slack QA artifacts."
expected_result="Slack native exec and plugin approval checkpoints pass"
screenshot_required=false
desktop_capture_inline=false
if [[ "$status" == "pass" ]]; then
checkpoint_required=true
fi
checkpoint_scenarios=()
if [[ "$scenario_label" == "approval-checkpoints" ]]; then
checkpoint_scenarios=("slack-approval-exec-native" "slack-approval-plugin-native")
else
checkpoint_scenarios=("$scenario_label")
fi
checkpoint_scenarios_json="$(printf '%s\n' "${checkpoint_scenarios[@]}" | jq -R . | jq -s .)"
checkpoint_artifacts="$(
jq -n \
--argjson checkpoint_required "$checkpoint_required" \
--argjson scenario_ids "$checkpoint_scenarios_json" \
'
def scenario_kind($id):
if $id == "slack-approval-exec-native" then "exec"
elif $id == "slack-approval-plugin-native" then "plugin"
else error("unsupported approval checkpoint scenario: \($id)")
end;
def scenario_title($id):
if scenario_kind($id) == "exec" then "Exec" else "Plugin" end;
[
$scenario_ids[] as $id
| ["pending", "resolved"][] as $state
| {
kind: "desktopScreenshot",
lane: "candidate",
label: "\(scenario_title($id)) approval \($state) checkpoint",
path: "approval-checkpoints/\($id)-\($state).png",
targetPath: "approval-checkpoints/\($id)-\($state).png",
alt: "Rendered Slack \(scenario_kind($id)) approval \($state) checkpoint",
width: 720,
inline: true,
required: $checkpoint_required
}
]
'
)"
fi
jq -n \
--arg status "$status" \
--arg candidate_sha "${{ needs.validate_ref.outputs.candidate_revision }}" \
--arg scenario "$SCENARIO_ID" \
--arg scenario "$scenario_label" \
--arg summary "$evidence_summary" \
--arg expected "$expected_result" \
--argjson checkpoint_artifacts "$checkpoint_artifacts" \
--argjson screenshot_required "$screenshot_required" \
--argjson desktop_capture_inline "$desktop_capture_inline" \
'{
schemaVersion: 1,
id: "slack-desktop-smoke",
title: "Mantis Slack Desktop Smoke QA",
summary: "Mantis ran Slack QA inside a Crabbox Linux VNC desktop, started an OpenClaw Slack gateway in that VM, opened Slack Web in the visible browser, and captured screenshot/video evidence.",
summary: $summary,
scenario: $scenario,
comparison: {
candidate: { sha: $candidate_sha, expected: "Slack QA and VM gateway setup pass", status: $status, fixed: ($status == "pass") },
candidate: { sha: $candidate_sha, expected: $expected, status: $status, fixed: ($status == "pass") },
pass: ($status == "pass")
},
artifacts: [
{ kind: "desktopScreenshot", lane: "candidate", label: "Slack desktop/VNC browser", path: "slack-desktop-smoke.png", targetPath: "slack-desktop.png", alt: "Slack Web desktop screenshot from the Mantis VM", width: 720, inline: true, required: $screenshot_required },
{ kind: "motionPreview", lane: "candidate", label: "Slack motion preview", path: "slack-desktop-smoke-preview.gif", targetPath: "slack-desktop-preview.gif", alt: "Animated Slack desktop preview", width: 720, inline: true, required: false },
artifacts: ([
{ kind: "desktopScreenshot", lane: "candidate", label: "Slack desktop/VNC browser", path: "slack-desktop-smoke.png", targetPath: "slack-desktop.png", alt: "Slack Web desktop screenshot from the Mantis VM", width: 720, inline: $desktop_capture_inline, required: $screenshot_required },
{ kind: "motionPreview", lane: "candidate", label: "Slack motion preview", path: "slack-desktop-smoke-preview.gif", targetPath: "slack-desktop-preview.gif", alt: "Animated Slack desktop preview", width: 720, inline: $desktop_capture_inline, required: false },
{ kind: "motionClip", lane: "candidate", label: "Slack change MP4", path: "slack-desktop-smoke-change.mp4", targetPath: "slack-desktop-change.mp4", required: false },
{ kind: "fullVideo", lane: "candidate", label: "Slack desktop MP4", path: "slack-desktop-smoke.mp4", targetPath: "slack-desktop.mp4", required: false },
{ kind: "metadata", lane: "run", label: "Slack desktop summary", path: "mantis-slack-desktop-smoke-summary.json", targetPath: "summary.json" },
@@ -342,7 +436,7 @@ jobs:
{ kind: "metadata", lane: "run", label: "Slack command log", path: "slack-desktop-command.log", targetPath: "slack-desktop-command.log", required: false },
{ kind: "metadata", lane: "run", label: "Slack preview metadata", path: "slack-desktop-smoke-preview.json", targetPath: "slack-desktop-preview.json", required: false },
{ kind: "metadata", lane: "run", label: "Slack error", path: "error.txt", targetPath: "error.txt", required: false }
]
] + $checkpoint_artifacts)
}' > "$root/mantis-evidence.json"
cat "$root/mantis-slack-desktop-smoke-report.md" >> "$GITHUB_STEP_SUMMARY"

View File

@@ -45,7 +45,6 @@ permissions:
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
NODE_VERSION: "24.x"
PNPM_VERSION: "11.0.8"
OPENCLAW_BUILD_PRIVATE_QA: "1"
OPENCLAW_ENABLE_PRIVATE_QA_CLI: "1"
CRABBOX_REF: main
@@ -356,7 +355,6 @@ jobs:
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
install-bun: "true"
- name: Setup Go for Crabbox CLI
@@ -618,7 +616,6 @@ jobs:
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
install-bun: "true"
- name: Download existing proof artifact

View File

@@ -41,7 +41,6 @@ permissions:
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
NODE_VERSION: "24.x"
PNPM_VERSION: "11.0.8"
OPENCLAW_BUILD_PRIVATE_QA: "1"
OPENCLAW_ENABLE_PRIVATE_QA_CLI: "1"
CRABBOX_REF: main
@@ -320,7 +319,6 @@ jobs:
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
install-bun: "true"
- name: Build Mantis harness

View File

@@ -104,7 +104,6 @@ concurrency:
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
NODE_VERSION: "24.15.0"
PNPM_VERSION: "11.0.8"
jobs:
run_package_telegram_e2e:
@@ -147,7 +146,6 @@ jobs:
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
install-bun: "true"
- name: Validate inputs and secrets

View File

@@ -193,7 +193,6 @@ concurrency:
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
NODE_VERSION: "24.15.0"
PNPM_VERSION: "11.0.8"
OPENCLAW_REPOSITORY: openclaw/openclaw
TSX_VERSION: "4.21.0"
OPENCLAW_CROSS_OS_OPENAI_MODEL: ${{ inputs.openai_model || vars.OPENCLAW_CROSS_OS_OPENAI_MODEL || 'openai/gpt-5.5' }}
@@ -339,7 +338,7 @@ jobs:
ref: ${{ steps.workflow_ref.outputs.value }}
path: workflow
fetch-depth: 1
persist-credentials: false
persist-credentials: true
- name: Checkout public source ref
if: inputs.candidate_artifact_name == ''
@@ -349,21 +348,21 @@ jobs:
ref: ${{ inputs.ref }}
path: source
fetch-depth: 0
persist-credentials: false
persist-credentials: true
submodules: recursive
- name: Setup pnpm
uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1
with:
version: ${{ env.PNPM_VERSION }}
run_install: false
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: ${{ env.NODE_VERSION }}
cache: pnpm
cache-dependency-path: ${{ inputs.candidate_artifact_name == '' && 'source/pnpm-lock.yaml' || 'workflow/pnpm-lock.yaml' }}
- name: Setup pnpm
uses: ./workflow/.github/actions/setup-pnpm-store-cache
with:
node-version: ${{ env.NODE_VERSION }}
package-manager-file: ${{ inputs.candidate_artifact_name == '' && 'source/package.json' || 'workflow/package.json' }}
lockfile-path: ${{ inputs.candidate_artifact_name == '' && 'source/pnpm-lock.yaml' || 'workflow/pnpm-lock.yaml' }}
use-actions-cache: ${{ inputs.candidate_artifact_name == '' && 'true' || 'false' }}
- name: Ensure pnpm store cache directory exists
run: mkdir -p "$(pnpm store path --silent)"
@@ -538,19 +537,21 @@ jobs:
ref: ${{ needs.prepare.outputs.workflow_ref }}
path: workflow
fetch-depth: 1
persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1
with:
version: ${{ env.PNPM_VERSION }}
run_install: false
persist-credentials: true
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: ${{ env.NODE_VERSION }}
- name: Setup pnpm
uses: ./workflow/.github/actions/setup-pnpm-store-cache
with:
node-version: ${{ env.NODE_VERSION }}
package-manager-file: workflow/package.json
lockfile-path: workflow/pnpm-lock.yaml
use-actions-cache: "false"
- name: Download candidate artifact
uses: actions/download-artifact@v8
with:

View File

@@ -219,6 +219,8 @@ on:
required: false
ANTHROPIC_API_TOKEN:
required: false
FACTORY_API_KEY:
required: false
BYTEPLUS_API_KEY:
required: false
CEREBRAS_API_KEY:
@@ -308,7 +310,6 @@ permissions:
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
NODE_VERSION: "24.15.0"
PNPM_VERSION: "11.0.8"
jobs:
validate_selected_ref:
@@ -491,7 +492,6 @@ jobs:
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
install-bun: "true"
- name: Validate live cache credentials
@@ -539,7 +539,6 @@ jobs:
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
install-bun: "true"
- name: Build dist for repo E2E
@@ -547,6 +546,9 @@ jobs:
NODE_OPTIONS: --max-old-space-size=8192
run: pnpm build
- name: Install Playwright Chromium
run: pnpm --dir ui exec playwright install --with-deps chromium
- name: Run repo E2E suite
run: pnpm test:e2e
@@ -581,7 +583,6 @@ jobs:
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
install-bun: "true"
- name: Build dist for special E2E
@@ -697,6 +698,7 @@ jobs:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
ANTHROPIC_API_TOKEN: ${{ secrets.ANTHROPIC_API_TOKEN }}
ANTHROPIC_API_KEY_OLD: ${{ secrets.ANTHROPIC_API_KEY_OLD }}
FACTORY_API_KEY: ${{ secrets.FACTORY_API_KEY }}
BYTEPLUS_API_KEY: ${{ secrets.BYTEPLUS_API_KEY }}
CEREBRAS_API_KEY: ${{ secrets.CEREBRAS_API_KEY }}
DEEPINFRA_API_KEY: ${{ secrets.DEEPINFRA_API_KEY }}
@@ -757,6 +759,7 @@ jobs:
if: contains(matrix.profiles, inputs.release_test_profile)
uses: actions/checkout@v6
with:
persist-credentials: false
ref: ${{ needs.validate_selected_ref.outputs.selected_sha }}
fetch-depth: 1
@@ -764,24 +767,23 @@ jobs:
if: contains(matrix.profiles, inputs.release_test_profile)
uses: actions/checkout@v6
with:
persist-credentials: false
ref: ${{ github.sha }}
fetch-depth: 1
path: .release-harness
- name: Log in to GHCR for shared Docker E2E image
if: contains(matrix.profiles, inputs.release_test_profile)
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ github.token }}
run: bash .release-harness/scripts/ci-docker-login-ghcr.sh
env:
GHCR_USERNAME: ${{ github.actor }}
GITHUB_TOKEN: ${{ github.token }}
- name: Setup Node environment
if: contains(matrix.profiles, inputs.release_test_profile)
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
install-bun: "true"
- name: Hydrate live auth/profile inputs
@@ -843,15 +845,35 @@ jobs:
run: |
set -euo pipefail
credentials=",$CREDENTIALS,"
if [[ "$credentials" == *",openai,"* ]]; then
[[ -n "${OPENAI_API_KEY:-}" ]] || {
echo "OPENAI_API_KEY is required for selected Docker E2E lanes." >&2
exit 1
}
fi
if [[ "$credentials" == *",anthropic,"* && -z "${ANTHROPIC_API_TOKEN:-}" && -z "${ANTHROPIC_API_KEY:-}" ]]; then
echo "ANTHROPIC_API_TOKEN or ANTHROPIC_API_KEY is required for selected Docker E2E lanes." >&2
require_any() {
local label="$1"
shift
local key
for key in "$@"; do
if [[ -n "${!key:-}" ]]; then
return 0
fi
done
echo "Missing credential for ${label}: expected one of $*" >&2
exit 1
}
if [[ "$credentials" == *",openai,"* ]]; then
require_any OpenAI OPENAI_API_KEY
fi
if [[ "$credentials" == *",codex,"* ]]; then
require_any Codex OPENCLAW_CODEX_AUTH_JSON
fi
if [[ "$credentials" == *",anthropic,"* ]]; then
require_any Anthropic ANTHROPIC_API_TOKEN ANTHROPIC_API_KEY OPENCLAW_CLAUDE_CREDENTIALS_JSON OPENCLAW_CLAUDE_JSON
fi
if [[ "$credentials" == *",factory,"* ]]; then
require_any Factory FACTORY_API_KEY
fi
if [[ "$credentials" == *",gemini,"* ]]; then
require_any Gemini GEMINI_API_KEY GOOGLE_API_KEY OPENCLAW_GEMINI_SETTINGS_JSON
fi
if [[ "$credentials" == *",opencode,"* ]]; then
require_any OpenCode OPENCODE_API_KEY OPENCODE_ZEN_API_KEY
fi
- name: Run Docker E2E chunk
@@ -907,6 +929,7 @@ jobs:
- name: Checkout trusted release harness
uses: actions/checkout@v6
with:
persist-credentials: false
ref: ${{ github.sha }}
fetch-depth: 1
@@ -939,6 +962,7 @@ jobs:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
ANTHROPIC_API_TOKEN: ${{ secrets.ANTHROPIC_API_TOKEN }}
ANTHROPIC_API_KEY_OLD: ${{ secrets.ANTHROPIC_API_KEY_OLD }}
FACTORY_API_KEY: ${{ secrets.FACTORY_API_KEY }}
BYTEPLUS_API_KEY: ${{ secrets.BYTEPLUS_API_KEY }}
CEREBRAS_API_KEY: ${{ secrets.CEREBRAS_API_KEY }}
DEEPINFRA_API_KEY: ${{ secrets.DEEPINFRA_API_KEY }}
@@ -997,28 +1021,28 @@ jobs:
- name: Checkout selected ref
uses: actions/checkout@v6
with:
persist-credentials: false
ref: ${{ needs.validate_selected_ref.outputs.selected_sha }}
fetch-depth: 1
- name: Checkout trusted release harness
uses: actions/checkout@v6
with:
persist-credentials: false
ref: ${{ github.sha }}
fetch-depth: 1
path: .release-harness
- name: Log in to GHCR for shared Docker E2E image
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ github.token }}
run: bash .release-harness/scripts/ci-docker-login-ghcr.sh
env:
GHCR_USERNAME: ${{ github.actor }}
GITHUB_TOKEN: ${{ github.token }}
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
install-bun: "true"
- name: Hydrate live auth/profile inputs
@@ -1081,15 +1105,35 @@ jobs:
run: |
set -euo pipefail
credentials=",$CREDENTIALS,"
if [[ "$credentials" == *",openai,"* ]]; then
[[ -n "${OPENAI_API_KEY:-}" ]] || {
echo "OPENAI_API_KEY is required for selected Docker E2E lanes." >&2
exit 1
}
fi
if [[ "$credentials" == *",anthropic,"* && -z "${ANTHROPIC_API_TOKEN:-}" && -z "${ANTHROPIC_API_KEY:-}" ]]; then
echo "ANTHROPIC_API_TOKEN or ANTHROPIC_API_KEY is required for selected Docker E2E lanes." >&2
require_any() {
local label="$1"
shift
local key
for key in "$@"; do
if [[ -n "${!key:-}" ]]; then
return 0
fi
done
echo "Missing credential for ${label}: expected one of $*" >&2
exit 1
}
if [[ "$credentials" == *",openai,"* ]]; then
require_any OpenAI OPENAI_API_KEY
fi
if [[ "$credentials" == *",codex,"* ]]; then
require_any Codex OPENCLAW_CODEX_AUTH_JSON
fi
if [[ "$credentials" == *",anthropic,"* ]]; then
require_any Anthropic ANTHROPIC_API_TOKEN ANTHROPIC_API_KEY OPENCLAW_CLAUDE_CREDENTIALS_JSON OPENCLAW_CLAUDE_JSON
fi
if [[ "$credentials" == *",factory,"* ]]; then
require_any Factory FACTORY_API_KEY
fi
if [[ "$credentials" == *",gemini,"* ]]; then
require_any Gemini GEMINI_API_KEY GOOGLE_API_KEY OPENCLAW_GEMINI_SETTINGS_JSON
fi
if [[ "$credentials" == *",opencode,"* ]]; then
require_any OpenCode OPENCODE_API_KEY OPENCODE_ZEN_API_KEY
fi
- name: Run targeted Docker E2E lanes
@@ -1165,17 +1209,15 @@ jobs:
path: .release-harness
- name: Log in to GHCR for shared Docker E2E image
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ github.token }}
run: bash .release-harness/scripts/ci-docker-login-ghcr.sh
env:
GHCR_USERNAME: ${{ github.actor }}
GITHUB_TOKEN: ${{ github.token }}
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
install-bun: "true"
- name: Validate Open WebUI credentials
@@ -1335,7 +1377,6 @@ jobs:
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
install-bun: "true"
- name: Download current-run OpenClaw Docker E2E package
@@ -1426,11 +1467,10 @@ jobs:
- name: Log in to GHCR
if: steps.plan.outputs.needs_e2e_image == '1'
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ github.token }}
run: bash .release-harness/scripts/ci-docker-login-ghcr.sh
env:
GHCR_USERNAME: ${{ github.actor }}
GITHUB_TOKEN: ${{ github.token }}
- name: Check existing shared Docker E2E images
id: image_exists
@@ -1541,11 +1581,10 @@ jobs:
echo "Shared live-test image: \`${live_image}\`" >> "$GITHUB_STEP_SUMMARY"
- name: Log in to GHCR
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ github.token }}
run: bash scripts/ci-docker-login-ghcr.sh
env:
GHCR_USERNAME: ${{ github.actor }}
GITHUB_TOKEN: ${{ github.token }}
- name: Check existing shared live-test image
id: image_exists
@@ -1679,7 +1718,6 @@ jobs:
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
install-bun: "true"
- name: Hydrate live auth/profile inputs
@@ -1688,11 +1726,10 @@ jobs:
- name: Log in to GHCR
if: contains(matrix.profiles, inputs.release_test_profile)
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ github.token }}
run: bash .release-harness/scripts/ci-docker-login-ghcr.sh
env:
GHCR_USERNAME: ${{ github.actor }}
GITHUB_TOKEN: ${{ github.token }}
- name: Validate provider credential
if: contains(matrix.profiles, inputs.release_test_profile)
@@ -1798,7 +1835,6 @@ jobs:
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
install-bun: "true"
- name: Normalize provider allowlist
@@ -1864,11 +1900,10 @@ jobs:
run: bash scripts/ci-hydrate-live-auth.sh
- name: Log in to GHCR
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ github.token }}
run: bash .release-harness/scripts/ci-docker-login-ghcr.sh
env:
GHCR_USERNAME: ${{ github.actor }}
GITHUB_TOKEN: ${{ github.token }}
- name: Validate provider credentials
shell: bash
@@ -2168,7 +2203,6 @@ jobs:
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
install-bun: "true"
- name: Hydrate live auth/profile inputs
@@ -2244,49 +2278,49 @@ jobs:
include:
- suite_id: live-gateway-docker
label: Docker live gateway OpenAI
command: OPENCLAW_LIVE_GATEWAY_PROVIDERS=openai 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
timeout_minutes: 30
command: OPENCLAW_LIVE_GATEWAY_PROVIDERS=openai OPENCLAW_LIVE_GATEWAY_MODELS=openai/gpt-5.5 OPENCLAW_LIVE_GATEWAY_MAX_MODELS=1 OPENCLAW_LIVE_GATEWAY_STEP_TIMEOUT_MS=90000 OPENCLAW_LIVE_GATEWAY_MODEL_TIMEOUT_MS=300000 OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" timeout --foreground --kill-after=30s 35m bash .release-harness/scripts/test-live-gateway-models-docker.sh
timeout_minutes: 40
profile_env_only: false
profiles: beta minimum stable full
- suite_id: live-gateway-anthropic-docker
label: Docker live gateway Anthropic
command: OPENCLAW_LIVE_GATEWAY_PROVIDERS=anthropic 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
timeout_minutes: 30
command: OPENCLAW_LIVE_GATEWAY_PROVIDERS=anthropic OPENCLAW_LIVE_GATEWAY_MAX_MODELS=2 OPENCLAW_LIVE_GATEWAY_STEP_TIMEOUT_MS=90000 OPENCLAW_LIVE_GATEWAY_MODEL_TIMEOUT_MS=180000 OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" timeout --foreground --kill-after=30s 35m bash .release-harness/scripts/test-live-gateway-models-docker.sh
timeout_minutes: 40
profile_env_only: false
profiles: stable full
- suite_id: live-gateway-google-docker
label: Docker live gateway Google
command: OPENCLAW_LIVE_GATEWAY_PROVIDERS=google OPENCLAW_LIVE_GATEWAY_MODELS=google/gemini-3.1-pro-preview,google/gemini-3-flash-preview 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
timeout_minutes: 30
command: OPENCLAW_LIVE_GATEWAY_PROVIDERS=google OPENCLAW_LIVE_GATEWAY_MODELS=google/gemini-3.1-pro-preview,google/gemini-3-flash-preview OPENCLAW_LIVE_GATEWAY_MAX_MODELS=2 OPENCLAW_LIVE_GATEWAY_STEP_TIMEOUT_MS=90000 OPENCLAW_LIVE_GATEWAY_MODEL_TIMEOUT_MS=180000 OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" timeout --foreground --kill-after=30s 35m bash .release-harness/scripts/test-live-gateway-models-docker.sh
timeout_minutes: 40
profile_env_only: false
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=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
command: OPENCLAW_LIVE_GATEWAY_PROVIDERS=minimax,minimax-portal OPENCLAW_LIVE_GATEWAY_MAX_MODELS=1 OPENCLAW_LIVE_GATEWAY_STEP_TIMEOUT_MS=90000 OPENCLAW_LIVE_GATEWAY_MODEL_TIMEOUT_MS=180000 OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" timeout --foreground --kill-after=30s 35m bash .release-harness/scripts/test-live-gateway-models-docker.sh
timeout_minutes: 40
profile_env_only: false
profiles: stable full
- suite_id: live-gateway-advisory-docker-deepseek-fireworks
suite_group: live-gateway-advisory-docker
label: Docker live gateway advisory DeepSeek/Fireworks
command: OPENCLAW_LIVE_GATEWAY_PROVIDERS=deepseek,fireworks 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
timeout_minutes: 30
command: OPENCLAW_LIVE_GATEWAY_PROVIDERS=deepseek,fireworks OPENCLAW_LIVE_GATEWAY_MAX_MODELS=2 OPENCLAW_LIVE_GATEWAY_STEP_TIMEOUT_MS=90000 OPENCLAW_LIVE_GATEWAY_MODEL_TIMEOUT_MS=180000 OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" timeout --foreground --kill-after=30s 35m bash .release-harness/scripts/test-live-gateway-models-docker.sh
timeout_minutes: 40
profile_env_only: false
advisory: true
profiles: full
- suite_id: live-gateway-advisory-docker-opencode-openrouter
suite_group: live-gateway-advisory-docker
label: Docker live gateway advisory OpenCode/OpenRouter
command: OPENCLAW_LIVE_GATEWAY_PROVIDERS=opencode-go,openrouter 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
timeout_minutes: 30
command: OPENCLAW_LIVE_GATEWAY_PROVIDERS=opencode-go,openrouter OPENCLAW_LIVE_GATEWAY_MAX_MODELS=2 OPENCLAW_LIVE_GATEWAY_STEP_TIMEOUT_MS=90000 OPENCLAW_LIVE_GATEWAY_MODEL_TIMEOUT_MS=180000 OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" timeout --foreground --kill-after=30s 35m bash .release-harness/scripts/test-live-gateway-models-docker.sh
timeout_minutes: 40
profile_env_only: false
advisory: true
profiles: full
- suite_id: live-gateway-advisory-docker-xai-zai
suite_group: live-gateway-advisory-docker
label: Docker live gateway advisory xAI/Z.ai
command: OPENCLAW_LIVE_GATEWAY_PROVIDERS=xai,zai 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
timeout_minutes: 30
command: OPENCLAW_LIVE_GATEWAY_PROVIDERS=xai,zai OPENCLAW_LIVE_GATEWAY_MAX_MODELS=2 OPENCLAW_LIVE_GATEWAY_STEP_TIMEOUT_MS=90000 OPENCLAW_LIVE_GATEWAY_MODEL_TIMEOUT_MS=180000 OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" timeout --foreground --kill-after=30s 35m bash .release-harness/scripts/test-live-gateway-models-docker.sh
timeout_minutes: 40
profile_env_only: false
advisory: true
profiles: full
@@ -2386,7 +2420,6 @@ jobs:
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
install-bun: "true"
- name: Hydrate live auth/profile inputs
@@ -2395,11 +2428,10 @@ jobs:
- name: Log in to GHCR
if: contains(matrix.profiles, inputs.release_test_profile) && (inputs.live_suite_filter == '' || inputs.live_suite_filter == matrix.suite_id || (inputs.live_suite_filter == 'live-gateway-advisory-docker' && startsWith(matrix.suite_id, 'live-gateway-advisory-docker-')))
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ github.token }}
run: bash .release-harness/scripts/ci-docker-login-ghcr.sh
env:
GHCR_USERNAME: ${{ github.actor }}
GITHUB_TOKEN: ${{ github.token }}
- name: Configure suite-specific env
if: contains(matrix.profiles, inputs.release_test_profile) && (inputs.live_suite_filter == '' || inputs.live_suite_filter == matrix.suite_id || (inputs.live_suite_filter == 'live-gateway-advisory-docker' && startsWith(matrix.suite_id, 'live-gateway-advisory-docker-')))
@@ -2605,7 +2637,6 @@ jobs:
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
install-bun: "true"
- name: Hydrate live auth/profile inputs

View File

@@ -35,13 +35,12 @@ on:
- latest
concurrency:
group: openclaw-npm-release-${{ github.event_name == 'workflow_dispatch' && format('{0}-{1}', inputs.tag, inputs.npm_dist_tag) || github.ref }}
group: ${{ github.event_name == 'workflow_dispatch' && inputs.preflight_only && format('openclaw-npm-release-{0}-{1}-preflight', inputs.tag, inputs.npm_dist_tag) || github.event_name == 'workflow_dispatch' && format('openclaw-npm-release-{0}-{1}-publish-{2}', inputs.tag, inputs.npm_dist_tag, github.run_id) || format('openclaw-npm-release-{0}', github.ref) }}
cancel-in-progress: ${{ github.event_name == 'workflow_dispatch' && inputs.preflight_only && inputs.npm_dist_tag == 'alpha' }}
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
NODE_VERSION: "24.15.0"
PNPM_VERSION: "11.0.8"
jobs:
# PLEASE DON'T ADD LONG-RUNNING OR FLAKY CHECKS TO THE npm RELEASE PATH.
@@ -118,7 +117,6 @@ jobs:
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
install-bun: "true"
- name: Ensure version is not already published
@@ -392,6 +390,8 @@ jobs:
- name: Require preflight artifact promotion on real publish
env:
RELEASE_TAG: ${{ inputs.tag }}
RELEASE_NPM_DIST_TAG: ${{ inputs.npm_dist_tag }}
PREFLIGHT_RUN_ID: ${{ inputs.preflight_run_id }}
FULL_RELEASE_VALIDATION_RUN_ID: ${{ inputs.full_release_validation_run_id }}
RELEASE_PUBLISH_RUN_ID: ${{ inputs.release_publish_run_id }}
@@ -402,8 +402,12 @@ jobs:
exit 1
fi
if [[ -z "${FULL_RELEASE_VALIDATION_RUN_ID}" ]]; then
echo "Real publish requires full_release_validation_run_id from a successful Full Release Validation run." >&2
exit 1
if [[ "${RELEASE_TAG}" == *"-beta."* && "${RELEASE_NPM_DIST_TAG}" == "beta" ]]; then
echo "::warning::Beta publish is proceeding from npm preflight only; full release validation remains required before stable/latest promotion."
else
echo "Real publish requires full_release_validation_run_id from a successful Full Release Validation run." >&2
exit 1
fi
fi
if [[ -z "${RELEASE_PUBLISH_RUN_ID// }" && "${GITHUB_ACTOR}" == "github-actions[bot]" ]]; then
echo "Workflow-dispatched real publish requires release_publish_run_id from the approved OpenClaw Release Publish workflow." >&2
@@ -495,7 +499,6 @@ jobs:
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
install-bun: "false"
- name: Ensure version is not already published
@@ -514,21 +517,20 @@ jobs:
env:
GH_TOKEN: ${{ github.token }}
PREFLIGHT_RUN_ID: ${{ inputs.preflight_run_id }}
EXPECTED_PREFLIGHT_BRANCH: ${{ github.ref_name }}
run: |
set -euo pipefail
RUN_JSON="$(gh run view "$PREFLIGHT_RUN_ID" --repo "$GITHUB_REPOSITORY" --json workflowName,headBranch,event,conclusion,url)"
printf '%s' "$RUN_JSON" | node -e 'const fs = require("node:fs"); const run = JSON.parse(fs.readFileSync(0, "utf8")); const checks = [["workflowName", "OpenClaw NPM Release"], ["headBranch", process.env.EXPECTED_PREFLIGHT_BRANCH], ["event", "workflow_dispatch"], ["conclusion", "success"]]; for (const [key, expected] of checks) { if (run[key] !== expected) { console.error(`Referenced npm preflight run ${process.env.PREFLIGHT_RUN_ID} must have ${key}=${expected}, got ${run[key] ?? "<missing>"}.`); process.exit(1); } } console.log(`Using npm preflight run ${process.env.PREFLIGHT_RUN_ID}: ${run.url}`);'
printf '%s' "$RUN_JSON" | node -e 'const fs = require("node:fs"); const run = JSON.parse(fs.readFileSync(0, "utf8")); const checks = [["workflowName", "OpenClaw NPM Release"], ["event", "workflow_dispatch"], ["conclusion", "success"]]; for (const [key, expected] of checks) { if (run[key] !== expected) { console.error(`Referenced npm preflight run ${process.env.PREFLIGHT_RUN_ID} must have ${key}=${expected}, got ${run[key] ?? "<missing>"}.`); process.exit(1); } } console.log(`Using npm preflight run ${process.env.PREFLIGHT_RUN_ID} from ${run.headBranch}: ${run.url}`);'
- name: Verify full release validation run metadata
if: ${{ inputs.full_release_validation_run_id != '' }}
env:
GH_TOKEN: ${{ github.token }}
FULL_RELEASE_VALIDATION_RUN_ID: ${{ inputs.full_release_validation_run_id }}
EXPECTED_WORKFLOW_BRANCH: ${{ github.ref_name }}
run: |
set -euo pipefail
RUN_JSON="$(gh run view "$FULL_RELEASE_VALIDATION_RUN_ID" --repo "$GITHUB_REPOSITORY" --json workflowName,headBranch,event,status,conclusion,url)"
printf '%s' "$RUN_JSON" | node -e 'const fs = require("node:fs"); const run = JSON.parse(fs.readFileSync(0, "utf8")); const checks = [["workflowName", "Full Release Validation"], ["headBranch", process.env.EXPECTED_WORKFLOW_BRANCH], ["event", "workflow_dispatch"], ["status", "completed"], ["conclusion", "success"]]; for (const [key, expected] of checks) { if (run[key] !== expected) { console.error(`Referenced full release validation run ${process.env.FULL_RELEASE_VALIDATION_RUN_ID} must have ${key}=${expected}, got ${run[key] ?? "<missing>"}.`); process.exit(1); } } console.log(`Using full release validation run ${process.env.FULL_RELEASE_VALIDATION_RUN_ID}: ${run.url}`);'
printf '%s' "$RUN_JSON" | node -e 'const fs = require("node:fs"); const run = JSON.parse(fs.readFileSync(0, "utf8")); const checks = [["workflowName", "Full Release Validation"], ["event", "workflow_dispatch"], ["status", "completed"], ["conclusion", "success"]]; for (const [key, expected] of checks) { if (run[key] !== expected) { console.error(`Referenced full release validation run ${process.env.FULL_RELEASE_VALIDATION_RUN_ID} must have ${key}=${expected}, got ${run[key] ?? "<missing>"}.`); process.exit(1); } } console.log(`Using full release validation run ${process.env.FULL_RELEASE_VALIDATION_RUN_ID} from ${run.headBranch}: ${run.url}`);'
- name: Download prepared npm tarball
env:
@@ -584,6 +586,7 @@ jobs:
download_preflight_artifact
- name: Download full release validation manifest
if: ${{ inputs.full_release_validation_run_id != '' }}
uses: actions/download-artifact@v8
with:
name: full-release-validation-${{ inputs.full_release_validation_run_id }}
@@ -649,6 +652,7 @@ jobs:
fi
- name: Verify full release validation target
if: ${{ inputs.full_release_validation_run_id != '' }}
run: |
set -euo pipefail
EXPECTED_RELEASE_SHA="$(git rev-parse HEAD)"

View File

@@ -91,7 +91,6 @@ concurrency:
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
NODE_VERSION: "24.15.0"
PNPM_VERSION: "11.0.8"
OPENCLAW_CI_OPENAI_MODEL: ${{ vars.OPENCLAW_CI_OPENAI_MODEL || 'openai/gpt-5.5' }}
jobs:
@@ -192,11 +191,21 @@ jobs:
working-directory: source
env:
RELEASE_REF: ${{ inputs.ref }}
GITHUB_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
SELECTED_SHA="$(git rev-parse HEAD)"
git fetch --no-tags origin '+refs/heads/*:refs/remotes/origin/*'
git fetch --tags origin '+refs/tags/*:refs/tags/*'
git_fetch_with_checkout_auth() {
if git config --get-all http.https://github.com/.extraheader >/dev/null; then
git fetch "$@"
return
fi
local auth_header
auth_header="$(printf 'x-access-token:%s' "$GITHUB_TOKEN" | base64 | tr -d '\n')"
git -c "http.https://github.com/.extraheader=AUTHORIZATION: basic ${auth_header}" fetch "$@"
}
git_fetch_with_checkout_auth --no-tags origin '+refs/heads/*:refs/remotes/origin/*'
git_fetch_with_checkout_auth --tags origin '+refs/tags/*:refs/tags/*'
if git tag --points-at "${SELECTED_SHA}" | grep -Eq '^v'; then
exit 0
@@ -239,6 +248,7 @@ jobs:
env:
SELECTED_SHA: ${{ steps.ref.outputs.sha }}
WORKFLOW_REF: ${{ github.ref }}
GITHUB_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
if [[ ! "${WORKFLOW_REF}" =~ ^refs/heads/tideclaw/alpha/[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{4}Z$ ]]; then
@@ -246,7 +256,16 @@ jobs:
exit 1
fi
alpha_branch="${WORKFLOW_REF#refs/heads/}"
git fetch --no-tags origin "+refs/heads/${alpha_branch}:refs/remotes/origin/${alpha_branch}"
git_fetch_with_checkout_auth() {
if git config --get-all http.https://github.com/.extraheader >/dev/null; then
git fetch "$@"
return
fi
local auth_header
auth_header="$(printf 'x-access-token:%s' "$GITHUB_TOKEN" | base64 | tr -d '\n')"
git -c "http.https://github.com/.extraheader=AUTHORIZATION: basic ${auth_header}" fetch "$@"
}
git_fetch_with_checkout_auth --no-tags origin "+refs/heads/${alpha_branch}:refs/remotes/origin/${alpha_branch}"
if ! git merge-base --is-ancestor "${SELECTED_SHA}" "refs/remotes/origin/${alpha_branch}"; then
echo "Alpha release target ${SELECTED_SHA} must be reachable from ${alpha_branch}." >&2
exit 1
@@ -475,7 +494,7 @@ jobs:
- name: Checkout trusted workflow ref
uses: actions/checkout@v6
with:
persist-credentials: false
persist-credentials: true
ref: ${{ github.ref_name }}
fetch-depth: 0
@@ -487,7 +506,6 @@ jobs:
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
install-bun: "true"
install-deps: "false"
@@ -598,6 +616,7 @@ jobs:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
ANTHROPIC_API_KEY_OLD: ${{ secrets.ANTHROPIC_API_KEY_OLD }}
ANTHROPIC_API_TOKEN: ${{ secrets.ANTHROPIC_API_TOKEN }}
FACTORY_API_KEY: ${{ secrets.FACTORY_API_KEY }}
BYTEPLUS_API_KEY: ${{ secrets.BYTEPLUS_API_KEY }}
CEREBRAS_API_KEY: ${{ secrets.CEREBRAS_API_KEY }}
DEEPINFRA_API_KEY: ${{ secrets.DEEPINFRA_API_KEY }}
@@ -672,14 +691,14 @@ jobs:
pull-requests: read
uses: ./.github/workflows/package-acceptance.yml
with:
advisory: ${{ startsWith(github.ref, 'refs/heads/tideclaw/alpha/') }}
advisory: false
workflow_ref: ${{ github.ref_name }}
source: ${{ (needs.resolve_target.outputs.package_acceptance_package_spec != '' || needs.resolve_target.outputs.release_package_spec != '') && 'npm' || 'artifact' }}
package_spec: ${{ needs.resolve_target.outputs.package_acceptance_package_spec || needs.resolve_target.outputs.release_package_spec || 'openclaw@beta' }}
artifact_name: ${{ needs.prepare_release_package.outputs.artifact_name }}
package_sha256: ${{ (needs.resolve_target.outputs.package_acceptance_package_spec == '' && needs.resolve_target.outputs.release_package_spec == '') && needs.prepare_release_package.outputs.package_sha256 || '' }}
suite_profile: custom
docker_lanes: doctor-switch update-channel-switch skill-install update-corrupt-plugin upgrade-survivor published-upgrade-survivor root-managed-vps-upgrade update-restart-auth plugins-offline plugin-update
docker_lanes: doctor-switch update-channel-switch skill-install update-corrupt-plugin upgrade-survivor published-upgrade-survivor root-managed-vps-upgrade update-restart-auth plugins-offline plugin-update plugin-binding-command-escape
published_upgrade_survivor_baselines: ${{ needs.resolve_target.outputs.run_release_soak == 'true' && 'last-stable-4 2026.4.23 2026.5.2 2026.4.15' || '' }}
published_upgrade_survivor_scenarios: ${{ needs.resolve_target.outputs.run_release_soak == 'true' && 'reported-issues' || '' }}
telegram_mode: mock-openai
@@ -690,6 +709,7 @@ jobs:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
ANTHROPIC_API_KEY_OLD: ${{ secrets.ANTHROPIC_API_KEY_OLD }}
ANTHROPIC_API_TOKEN: ${{ secrets.ANTHROPIC_API_TOKEN }}
FACTORY_API_KEY: ${{ secrets.FACTORY_API_KEY }}
BYTEPLUS_API_KEY: ${{ secrets.BYTEPLUS_API_KEY }}
CEREBRAS_API_KEY: ${{ secrets.CEREBRAS_API_KEY }}
DEEPINFRA_API_KEY: ${{ secrets.DEEPINFRA_API_KEY }}
@@ -765,7 +785,7 @@ jobs:
- name: Checkout selected ref
uses: actions/checkout@v6
with:
persist-credentials: false
persist-credentials: true
ref: ${{ needs.resolve_target.outputs.revision }}
fetch-depth: 1
@@ -773,7 +793,6 @@ jobs:
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
install-bun: "true"
- name: Build private QA runtime
@@ -837,7 +856,7 @@ jobs:
- name: Checkout selected ref
uses: actions/checkout@v6
with:
persist-credentials: false
persist-credentials: true
ref: ${{ needs.resolve_target.outputs.revision }}
fetch-depth: 1
@@ -845,7 +864,6 @@ jobs:
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
install-bun: "true"
- name: Download parity lane artifacts
@@ -903,7 +921,7 @@ jobs:
- name: Checkout selected ref
uses: actions/checkout@v6
with:
persist-credentials: false
persist-credentials: true
ref: ${{ needs.resolve_target.outputs.revision }}
fetch-depth: 1
@@ -911,7 +929,6 @@ jobs:
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
install-bun: "true"
- name: Build private QA runtime
@@ -1019,7 +1036,7 @@ jobs:
- name: Checkout selected ref
uses: actions/checkout@v6
with:
persist-credentials: false
persist-credentials: true
ref: ${{ needs.resolve_target.outputs.revision }}
fetch-depth: 1
@@ -1027,7 +1044,6 @@ jobs:
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
install-bun: "true"
- name: Download runtime parity artifacts
@@ -1072,7 +1088,7 @@ jobs:
- name: Checkout selected ref
uses: actions/checkout@v6
with:
persist-credentials: false
persist-credentials: true
ref: ${{ needs.resolve_target.outputs.revision }}
fetch-depth: 1
@@ -1080,7 +1096,6 @@ jobs:
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
install-bun: "true"
- name: Build private QA runtime
@@ -1152,7 +1167,7 @@ jobs:
- name: Checkout selected ref
uses: actions/checkout@v6
with:
persist-credentials: false
persist-credentials: true
ref: ${{ needs.resolve_target.outputs.revision }}
fetch-depth: 1
@@ -1160,7 +1175,6 @@ jobs:
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
install-bun: "true"
- name: Validate required QA credential env
@@ -1248,7 +1262,7 @@ jobs:
- name: Checkout selected ref
uses: actions/checkout@v6
with:
persist-credentials: false
persist-credentials: true
ref: ${{ needs.resolve_target.outputs.revision }}
fetch-depth: 1
@@ -1256,7 +1270,6 @@ jobs:
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
install-bun: "true"
- name: Validate required QA credential env
@@ -1347,7 +1360,7 @@ jobs:
- name: Checkout selected ref
uses: actions/checkout@v6
with:
persist-credentials: false
persist-credentials: true
ref: ${{ needs.resolve_target.outputs.revision }}
fetch-depth: 1
@@ -1355,7 +1368,6 @@ jobs:
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
install-bun: "true"
- name: Validate required QA credential env
@@ -1443,7 +1455,7 @@ jobs:
- name: Checkout selected ref
uses: actions/checkout@v6
with:
persist-credentials: false
persist-credentials: true
ref: ${{ needs.resolve_target.outputs.revision }}
fetch-depth: 1
@@ -1451,7 +1463,6 @@ jobs:
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
install-bun: "true"
- name: Validate required QA credential env
@@ -1577,7 +1588,7 @@ jobs:
if [[ "$result" != "success" && "$result" != "skipped" ]]; then
if [[ "$tideclaw_alpha" == "true" ]]; then
case "$name" in
prepare_release_package|install_smoke_release_checks) ;;
prepare_release_package|install_smoke_release_checks|package_acceptance_release_checks) ;;
*)
echo "::warning::${name} ended with ${result}; Tideclaw alpha treats non-package-safety release-check lanes as advisory."
continue

View File

@@ -71,7 +71,6 @@ concurrency:
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
NODE_VERSION: "24.15.0"
PNPM_VERSION: "11.0.8"
jobs:
resolve_release_target:
@@ -361,7 +360,6 @@ jobs:
uses: ./.github/actions/setup-node-env
with:
install-bun: "false"
cache-key-suffix: release-publish
- name: Dispatch publish workflows
env:

View File

@@ -38,6 +38,7 @@ jobs:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
ANTHROPIC_API_KEY_OLD: ${{ secrets.ANTHROPIC_API_KEY_OLD }}
ANTHROPIC_API_TOKEN: ${{ secrets.ANTHROPIC_API_TOKEN }}
FACTORY_API_KEY: ${{ secrets.FACTORY_API_KEY }}
BYTEPLUS_API_KEY: ${{ secrets.BYTEPLUS_API_KEY }}
CEREBRAS_API_KEY: ${{ secrets.CEREBRAS_API_KEY }}
DEEPINFRA_API_KEY: ${{ secrets.DEEPINFRA_API_KEY }}

View File

@@ -17,6 +17,7 @@ on:
- npm
- ref
- url
- trusted-url
- artifact
package_ref:
description: Trusted package source ref when source=ref
@@ -29,12 +30,17 @@ on:
default: openclaw@beta
type: string
package_url:
description: HTTPS .tgz URL when source=url
description: HTTPS .tgz URL when source=url or source=trusted-url
required: false
default: ""
type: string
package_sha256:
description: Expected package SHA-256; required for source=url
description: Expected package SHA-256; required for source=url or source=trusted-url
required: false
default: ""
type: string
trusted_source_id:
description: Named trusted source policy when source=trusted-url
required: false
default: ""
type: string
@@ -111,7 +117,7 @@ on:
default: main
type: string
source:
description: "Package candidate source: npm, ref, url, or artifact"
description: "Package candidate source: npm, ref, url, trusted-url, or artifact"
required: true
type: string
package_ref:
@@ -125,12 +131,17 @@ on:
default: openclaw@beta
type: string
package_url:
description: HTTPS .tgz URL when source=url
description: HTTPS .tgz URL when source=url or source=trusted-url
required: false
default: ""
type: string
package_sha256:
description: Expected package SHA-256; required for source=url
description: Expected package SHA-256; required for source=url or source=trusted-url
required: false
default: ""
type: string
trusted_source_id:
description: Named trusted source policy when source=trusted-url
required: false
default: ""
type: string
@@ -180,6 +191,8 @@ on:
default: ""
type: string
secrets:
OPENCLAW_TRUSTED_PACKAGE_TOKEN:
required: false
OPENAI_API_KEY:
required: false
OPENAI_BASE_URL:
@@ -190,6 +203,8 @@ on:
required: false
ANTHROPIC_API_TOKEN:
required: false
FACTORY_API_KEY:
required: false
BYTEPLUS_API_KEY:
required: false
CEREBRAS_API_KEY:
@@ -288,7 +303,6 @@ concurrency:
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
NODE_VERSION: "24.15.0"
PNPM_VERSION: "11.0.8"
PACKAGE_ARTIFACT_NAME: package-under-test
jobs:
@@ -320,7 +334,6 @@ jobs:
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
install-bun: ${{ inputs.source == 'ref' && 'true' || 'false' }}
install-deps: "false"
@@ -355,6 +368,8 @@ jobs:
PACKAGE_SPEC: ${{ inputs.package_spec }}
PACKAGE_URL: ${{ inputs.package_url }}
PACKAGE_SHA256: ${{ inputs.package_sha256 }}
TRUSTED_SOURCE_ID: ${{ inputs.trusted_source_id }}
OPENCLAW_TRUSTED_PACKAGE_TOKEN: ${{ secrets.OPENCLAW_TRUSTED_PACKAGE_TOKEN }}
shell: bash
run: |
set -euo pipefail
@@ -369,6 +384,7 @@ jobs:
--package-spec "$PACKAGE_SPEC" \
--package-url "$PACKAGE_URL" \
--package-sha256 "$PACKAGE_SHA256" \
--trusted-source-id "$TRUSTED_SOURCE_ID" \
--artifact-dir "${artifact_dir:-.}" \
--output-dir .artifacts/docker-e2e-package \
--output-name openclaw-current.tgz \
@@ -490,6 +506,7 @@ jobs:
PACKAGE_SHA256: ${{ steps.resolve.outputs.sha256 }}
PACKAGE_VERSION: ${{ steps.resolve.outputs.package_version }}
PACKAGE_REF: ${{ inputs.package_ref }}
TRUSTED_SOURCE_ID: ${{ inputs.trusted_source_id }}
SOURCE: ${{ inputs.source }}
SUITE_PROFILE: ${{ inputs.suite_profile }}
WORKFLOW_REF: ${{ inputs.workflow_ref }}
@@ -506,6 +523,9 @@ jobs:
if [[ "${SOURCE}" == "ref" ]]; then
echo "- Package ref: \`${PACKAGE_REF}\`"
fi
if [[ "${SOURCE}" == "trusted-url" ]]; then
echo "- Trusted source: \`${TRUSTED_SOURCE_ID}\`"
fi
echo "- Version: \`${PACKAGE_VERSION}\`"
echo "- SHA-256: \`${PACKAGE_SHA256}\`"
echo "- Profile: \`${SUITE_PROFILE}\`"
@@ -514,9 +534,40 @@ jobs:
echo "- Published upgrade survivor scenarios: \`${PUBLISHED_UPGRADE_SURVIVOR_SCENARIOS}\`"
} >> "$GITHUB_STEP_SUMMARY"
package_integrity:
name: Package integrity
needs: resolve_package
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- name: Checkout package workflow ref
uses: actions/checkout@v6
with:
ref: ${{ inputs.workflow_ref }}
fetch-depth: 1
- name: Download package-under-test artifact
uses: actions/download-artifact@v8
with:
name: ${{ needs.resolve_package.outputs.package_artifact_name }}
path: .artifacts/docker-e2e-package
- name: Enforce public package integrity
env:
OPENCLAW_PACKAGE_TARBALL_CHECK_TIMINGS: "0"
shell: bash
run: |
set -euo pipefail
node scripts/check-openclaw-package-tarball.mjs .artifacts/docker-e2e-package/openclaw-current.tgz
docker_acceptance:
name: Docker product acceptance
needs: resolve_package
needs: [resolve_package, package_integrity]
permissions:
actions: read
contents: read
packages: write
pull-requests: read
uses: ./.github/workflows/openclaw-live-and-e2e-checks-reusable.yml
with:
advisory: ${{ inputs.advisory }}
@@ -537,6 +588,7 @@ jobs:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
ANTHROPIC_API_KEY_OLD: ${{ secrets.ANTHROPIC_API_KEY_OLD }}
ANTHROPIC_API_TOKEN: ${{ secrets.ANTHROPIC_API_TOKEN }}
FACTORY_API_KEY: ${{ secrets.FACTORY_API_KEY }}
BYTEPLUS_API_KEY: ${{ secrets.BYTEPLUS_API_KEY }}
CEREBRAS_API_KEY: ${{ secrets.CEREBRAS_API_KEY }}
DEEPINFRA_API_KEY: ${{ secrets.DEEPINFRA_API_KEY }}
@@ -580,7 +632,7 @@ jobs:
package_telegram:
name: Telegram package acceptance
needs: resolve_package
needs: [resolve_package, package_integrity]
if: needs.resolve_package.outputs.telegram_enabled == 'true'
uses: ./.github/workflows/npm-telegram-beta-e2e.yml
with:
@@ -598,7 +650,7 @@ jobs:
summary:
name: Verify package acceptance
needs: [resolve_package, docker_acceptance, package_telegram]
needs: [resolve_package, package_integrity, docker_acceptance, package_telegram]
if: always()
runs-on: ubuntu-24.04
timeout-minutes: 5
@@ -606,6 +658,7 @@ jobs:
- name: Verify package acceptance results
env:
DOCKER_RESULT: ${{ needs.docker_acceptance.result }}
PACKAGE_INTEGRITY_RESULT: ${{ needs.package_integrity.result }}
PACKAGE_TELEGRAM_RESULT: ${{ needs.package_telegram.result }}
RESOLVE_RESULT: ${{ needs.resolve_package.result }}
shell: bash
@@ -615,6 +668,7 @@ jobs:
failed=0
for item in \
"resolve_package=${RESOLVE_RESULT}" \
"package_integrity=${PACKAGE_INTEGRITY_RESULT}" \
"docker_acceptance=${DOCKER_RESULT}" \
"package_telegram=${PACKAGE_TELEGRAM_RESULT}"
do

View File

@@ -32,7 +32,6 @@ concurrency:
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
NODE_VERSION: "24.15.0"
PNPM_VERSION: "11.0.8"
CLAWHUB_REGISTRY: "https://clawhub.ai"
CLAWHUB_REPOSITORY: "openclaw/clawhub"
# Pinned to a reviewed ClawHub commit so release behavior stays reproducible.
@@ -61,7 +60,6 @@ jobs:
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
install-bun: "false"
- name: Resolve checked-out ref
@@ -264,7 +262,6 @@ jobs:
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
install-bun: "true"
install-deps: "true"
@@ -359,7 +356,6 @@ jobs:
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
install-bun: "true"
install-deps: "true"

View File

@@ -44,7 +44,6 @@ concurrency:
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
NODE_VERSION: "24.15.0"
PNPM_VERSION: "11.0.8"
jobs:
preview_plugins_npm:
@@ -68,7 +67,6 @@ jobs:
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
install-bun: "false"
- name: Resolve checked-out ref
@@ -230,7 +228,6 @@ jobs:
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
install-bun: "false"
- name: Preview publish command
@@ -264,7 +261,6 @@ jobs:
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
install-bun: "false"
- name: Ensure version is not already published

View File

@@ -52,7 +52,7 @@ jobs:
ref: ${{ inputs.target_ref }}
fetch-depth: 1
fetch-tags: false
persist-credentials: false
persist-credentials: true
submodules: false
- name: Build plugin prerelease manifest
@@ -221,7 +221,7 @@ jobs:
ref: ${{ needs.preflight.outputs.checkout_revision }}
fetch-depth: 1
fetch-tags: false
persist-credentials: false
persist-credentials: true
submodules: false
- name: Setup Node environment
@@ -257,7 +257,7 @@ jobs:
ref: ${{ needs.preflight.outputs.checkout_revision }}
fetch-depth: 1
fetch-tags: false
persist-credentials: false
persist-credentials: true
submodules: false
- name: Setup Node environment
@@ -330,7 +330,7 @@ jobs:
ref: ${{ needs.preflight.outputs.checkout_revision }}
fetch-depth: 1
fetch-tags: false
persist-credentials: false
persist-credentials: true
submodules: false
- name: Setup Node environment
@@ -362,7 +362,7 @@ jobs:
ref: ${{ needs.preflight.outputs.checkout_revision }}
fetch-depth: 1
fetch-tags: false
persist-credentials: false
persist-credentials: true
submodules: false
- name: Setup Node environment

View File

@@ -51,7 +51,6 @@ concurrency:
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
NODE_VERSION: "24.x"
PNPM_VERSION: "11.0.8"
OPENCLAW_CI_OPENAI_MODEL: ${{ vars.OPENCLAW_CI_OPENAI_MODEL || 'openai/gpt-5.5' }}
OPENCLAW_BUILD_PRIVATE_QA: "1"
OPENCLAW_ENABLE_PRIVATE_QA_CLI: "1"
@@ -182,7 +181,6 @@ jobs:
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
install-bun: "true"
- name: Build private QA runtime
@@ -252,7 +250,6 @@ jobs:
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
install-bun: "true"
- name: Validate required QA credential env
@@ -338,7 +335,6 @@ jobs:
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
install-bun: "true"
- name: Validate required QA credential env
@@ -424,7 +420,6 @@ jobs:
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
install-bun: "true"
- name: Validate required QA credential env
@@ -499,7 +494,6 @@ jobs:
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
install-bun: "true"
- name: Validate required QA credential env
@@ -594,7 +588,6 @@ jobs:
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
install-bun: "true"
- name: Validate required QA credential env
@@ -692,7 +685,6 @@ jobs:
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
install-bun: "true"
- name: Validate required QA credential env
@@ -787,7 +779,6 @@ jobs:
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
install-bun: "true"
- name: Validate required QA credential env

41
.github/workflows/tui-pty.yml vendored Normal file
View File

@@ -0,0 +1,41 @@
name: TUI PTY
on:
pull_request:
paths:
- "src/tui/**"
- "scripts/dev/tui-pty-test-watch.ts"
- "scripts/test-projects.test-support.mjs"
- "package.json"
- "pnpm-lock.yaml"
- "test/scripts/test-projects.test.ts"
- "test/vitest/vitest.test-shards.mjs"
- "test/vitest/vitest.tui-pty.config.ts"
- ".github/workflows/tui-pty.yml"
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
jobs:
tui-pty:
runs-on: ubuntu-24.04
timeout-minutes: 5
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
with:
install-bun: "false"
- name: Run TUI PTY tests
run: timeout 120s node scripts/run-vitest.mjs run --config test/vitest/vitest.tui-pty.config.ts

8
.gitignore vendored
View File

@@ -108,7 +108,9 @@ USER.md
.vscode/
# local tooling
.antigravitycli/
.serena/
.crabbox/
# local QA evidence mirrors; CI publishes canonical Mantis files as Actions artifacts
mantis/
@@ -122,6 +124,8 @@ mantis/
!.agents/skills/crabbox/**
!.agents/skills/clawdtributor/
!.agents/skills/clawdtributor/**
!.agents/skills/control-ui-e2e/
!.agents/skills/control-ui-e2e/**
!.agents/skills/gitcrawl/
!.agents/skills/gitcrawl/**
!.agents/skills/openclaw-docs/**
@@ -131,6 +135,8 @@ mantis/
!.agents/skills/openclaw-debugging/**
!.agents/skills/openclaw-ghsa-maintainer/
!.agents/skills/openclaw-ghsa-maintainer/**
!.agents/skills/openclaw-landable-bug-sweep/
!.agents/skills/openclaw-landable-bug-sweep/**
!.agents/skills/openclaw-parallels-smoke/
!.agents/skills/openclaw-parallels-smoke/**
!.agents/skills/openclaw-pr-maintainer/
@@ -159,6 +165,8 @@ mantis/
!.agents/skills/security-triage/**
!.agents/skills/tag-duplicate-prs-issues/
!.agents/skills/tag-duplicate-prs-issues/**
!.agents/skills/autoreview/
!.agents/skills/autoreview/**
# Agent credentials and memory (NEVER COMMIT)
/memory/

View File

@@ -17,12 +17,33 @@ Skills own workflows; root owns hard policy and routing.
- New channel/plugin/app/doc surface: update `.github/labeler.yml` + GH labels.
- New `AGENTS.md`: add sibling `CLAUDE.md` symlink; edit `AGENTS.md` only.
## ClawSweeper Review Policy
- OpenClaw-specific review rules live here; generic ClawSweeper prompts stay repo-agnostic.
- ClawSweeper-owned schema, labels, close reasons, protected-label gates, maintainer-item gates, and mutation rules live in `openclaw/clawsweeper`.
- Review workers read this full root `AGENTS.md` before judging; no reliance on search snippets, `head`, partial ranges, local excerpts, or truncated copies. Then read every scoped `AGENTS.md` that owns touched paths.
- Optional integrations, providers, channels, skill bundles, MCP surfaces, and service workflows route to plugins, ClawHub, or owner repos when current seams suffice. Keep core items for missing core/plugin APIs, bundled regressions, security/core hardening, or maintainer product decisions.
- Plugin APIs, provider routing, auth/session state, persisted preferences, config loading, migrations, setup, startup checks, and fallback behavior are compatibility/upgrade-sensitive. Treat config breaks, removed fallbacks, fail-closed changes, or new operator action as merge risk even with green CI.
- Review whole decision surfaces, not only the touched runtime, provider, channel, harness, plugin seam, or context path. Check sibling Codex/Pi-style runtimes, provider/model routing, channel delivery, gateway/protocol, plugin SDK, and context-management paths when relevant.
- One-sided fixes need sibling-surface proof, an explanation for why siblings are unaffected, or explicit follow-up work.
- User-facing `fix`, `feat`, and `perf` changes need `CHANGELOG.md` before landing; contributor PR authors are not blocked solely on maintainer-owned changelog work. Never request thanks for bot/forbidden handles: `@openclaw`, `@clawsweeper`, `@codex`, `@steipete`.
- Public ClawSweeper comments prefer `https://docs.openclaw.ai/...` when a public docs page exists; structured evidence still cites repo files, lines, SHAs.
- Findings need current source, shipped/current behavior, tests/CI evidence, and dependency contract proof when dependency-backed behavior is involved. Validation is judged against touched and sibling surfaces plus this file's commands; real behavior proof matters for user-visible changes, with Telegram/Desktop proof for Telegram-visible behavior when feasible.
- Prefer findings for concrete behavior regressions, missing changed-surface proof, owner-boundary violations, security/API contract issues, or docs/config mismatches.
- Do not file findings for repo policy preference when changed code follows the relevant scoped guide and no user-visible, runtime, security, or maintainer-risk impact is shown.
## Map
- Core TS: `src/`, `ui/`, `packages/`; plugins: `extensions/`; SDK: `src/plugin-sdk/*`; channels: `src/channels/*`; loader: `src/plugins/*`; protocol: `src/gateway/protocol/*`; docs/apps: `docs/`, `apps/`.
- Installers: sibling `../openclaw.ai`.
- Scoped guides: `extensions/`, `src/{plugin-sdk,channels,plugins,gateway,gateway/protocol,agents}/`, `test/helpers*/`, `docs/`, `ui/`, `scripts/`.
## Docs
- Source docs: `docs/**`; publish repo: `openclaw/docs`; host: `https://docs.openclaw.ai`.
- Flow: source -> `docs-sync-publish.yml` -> mirror build -> R2 -> Worker router.
- Docs AI: `openclaw/ask-molty`; see its `AGENTS.md`.
## Architecture
- Core stays plugin-agnostic. No bundled ids/defaults/policy in core when manifest/registry/capability contracts work.
@@ -43,6 +64,9 @@ Skills own workflows; root owns hard policy and routing.
- Channels are implementation under `src/channels/**`; plugin authors get SDK seams. Providers own auth/catalog/runtime hooks; core owns generic loop.
- Hot paths should carry prepared facts forward: provider id, model ref, channel id, target, capability family, attachment class. Do not rediscover with broad plugin/provider/channel/capability loaders.
- Do not fix repeated request-time discovery with scattered caches. Move the canonical fact earlier; reuse prepared runtime objects; delete duplicate lookup branches.
- Gateway/plugin metadata is process-stable: installs, manifests, catalogs, generated paths, bundled metadata. Changes require restart or explicit owner reload/install/doctor flow.
- Runtime hot paths: no freshness polling (`stat`/`realpath`/JSON reread/hash). Reuse current snapshots, install records, discovery, lookup tables, root scopes, resolved paths.
- Process-local metadata caches ok when lifecycle-owned and bounded/single-slot. Freshness exceptions need named owner + tests.
- Inline code comments: brief notes for tricky, bug-prone, or previously buggy logic.
- Gateway protocol changes: additive first; incompatible needs versioning/docs/client follow-through.
- Protocol version bumps: explicit owner confirmation only; never automatic/generated.
@@ -161,6 +185,7 @@ Skills own workflows; root owns hard policy and routing.
- Never commit real phone numbers, videos, credentials, live config.
- Secrets: channel/provider creds in `~/.openclaw/credentials/`; model auth profiles in `~/.openclaw/agents/<agentId>/agent/auth-profiles.json`.
- Dependency patches/overrides/vendor changes need explicit approval. `pnpm-workspace.yaml` patched dependencies use exact versions only.
- Lockfiles/shrinkwrap are security surface: review `pnpm-lock.yaml`, `npm-shrinkwrap.json`, `package-lock.json`; root/plugin npm packages ship shrinkwrap, not package-lock.
- Carbon pins owner-only: do not change `@buape/carbon` unless Shadow (`@thewilloftheshadow`, verified by `gh`) asks.
- Releases/publish/version bumps need explicit approval. Use `$openclaw-release-maintainer`.
- GHSA/advisories: `$openclaw-ghsa-maintainer` / `$security-triage`. Secret scanning: `$openclaw-secret-scanning-maintainer`.

View File

@@ -6,13 +6,119 @@ Docs: https://docs.openclaw.ai
### Changes
### Fixes
- Scripts: remove stale Knip unused-file allowlist entries so the dead-code gate fails only on current findings.
- Tests: normalize bundled plugin lifecycle probe paths and state-root lookup so native Windows release sweeps accept valid packaged plugin installs.
- Config: keep benign legacy metadata write anomalies out of default doctor and config command output while preserving explicit anomaly logging for diagnostics.
- Agents/Codex: route budget preflight compaction through the persisted Codex session model so Slack threads do not require separate plain OpenAI auth. Thanks @amknight.
- Codex: log when implicit app-server `never` approvals are promoted for OpenClaw tool policy, including whether the trigger was a `before_tool_call` hook or trusted tool policy.
- Google Vertex: support production ADC modes such as Workload Identity Federation, service-account credentials, and metadata-server ADC for the native Vertex transport. (#83971) Thanks @damianFelixPago.
- Telegram: route normal `[telegram][diag]` polling diagnostics through `runtime.log` while keeping non-diag warnings and persistence failures on `runtime.error`, so healthy polling startup no longer looks like an error. Fixes #82957. (#82958) Thanks @galiniliev.
## 2026.5.25
### Fixes
- Installer: let the local-prefix CLI installer use Alpine's `apk` Node.js, npm, and Git packages on musl Linux instead of downloading glibc Node tarballs that fail `node:sqlite`.
- Scripts: use `git grep` to prefilter tracked conflict-marker scans so changed checks avoid reading every repository file on clean runs.
- Plugins: allow linked local plugin paths to probe TypeScript source entries without requiring compiled package output, restoring source-checkout plugin development on native Windows.
- CLI: route source-checkout build output to stderr before launching OpenClaw commands so stale local builds do not corrupt `--json` stdout.
- Installer: install Node.js through `apk` on Alpine Linux instead of falling through to the NodeSource package-manager path.
- Agents/perf: cache manifest-backed CLI provider descriptors and fallback provider resolution so model fallback retries avoid repeated bundled provider runtime scans while still invalidating across plugin reloads.
- Installer: detect musl Linux shells such as Alpine as Linux instead of rejecting them before npm install.
- Scripts: run direct Node package scripts with env overrides through a cross-platform launcher so gateway, TUI, and Docker-all entrypoints work on native Windows.
- Tests: run Vitest import timing entrypoints through a Node wrapper so native Windows package scripts can collect import diagnostics.
- Control UI: split large build-time runtime dependencies into stable chunks so Linux/Docker install and package builds stay below the app chunk warning threshold.
- Tests: run `test:max` and `test:changed:max` through a Node wrapper so high-worker Vitest entrypoints work on native Windows.
- Tests: retry transient loopback HTTP resets in the kitchen-sink RPC walk so native Windows readiness probes do not fail after the gateway is already ready.
- Tests: run `test:serial` through a Node wrapper so targeted serial Vitest commands work on native Windows.
- Tests: normalize Vitest config path assertions so the infra config suite runs on native Windows paths.
- Scripts: run the optional Discord native opus installer through the shared pnpm launcher and Windows CI coverage so native Windows installs avoid shell-mode package-manager shims.
- Installer: avoid the incompatible generated `--before` install filter when raw npm `min-release-age` config is present. (#85491) Thanks @TurboTheTurtle.
- Agents/MCP: bound bundled MCP `tools/list` catalog discovery so hung MCP servers do not block session tool materialization. (#85063) Thanks @nxmxbbd.
- Scripts: run generated-module formatting through the shared pnpm launcher and Windows CI coverage so native Windows generator checks avoid shell-mode package-manager shims.
- Channels/iMessage: recover malformed anchorless group watch payloads by GUID before debounce/routing, and drop unrecoverable payloads instead of replying to the sender DM. Fixes #84470. Refs #84503. Thanks @zhangguiping-xydt and @zqchris.
- Channels/iMessage: advance the startup catchup cursor from live-handled rows after a completed catchup pass, including rows received while catchup is still running, so restarts do not replay them. (#85475) Thanks @TurboTheTurtle.
- Tests: mount the shared Windows command helper into bare Docker E2E harness containers so published upgrade-survivor config walks can start on Linux.
- Tests: keep the plugin binding command escape Docker smoke focused on its intended Vitest cases and skip source-only install lifecycle scripts.
- Tests: let the generic plugin install E2E assertions use a configurable temp root and Windows home-relative install paths.
- Tests: keep kitchen-sink plugin assertion fixtures on a configurable temp root so native Windows runs no longer skip full-surface diagnostic coverage.
- Tests: fail Gateway startup benchmarks when a child startup never produces ready probes or process metrics instead of reporting all `n/a` samples as passing.
- Config/secrets: allow exec SecretRef ids to include `#` selectors so AWS-style `secret#json_key` ids validate consistently. (#80731) Thanks @TurboTheTurtle.
- Tests: keep the Telegram user credential helper on platform temp and path APIs so native Windows credential export and restore commands do not write through POSIX-only paths.
- Installer: include the optional verify phase in the progress counter so `--verify` shows `[4/4] Verifying installation` instead of `[4/3]`.
- Crabbox: let the wrapper find a sibling Crabbox checkout from linked Git worktrees so Codex worktrees can run remote gates without a PATH shim.
- Scripts: tolerate the standard `--` option separator in shared script flag parsing so perf/test helpers accept package-manager argument forwarding.
- Tests: preserve `--` passthrough arguments in live-media, live-shard, and extension batch harnesses so Vitest filters are not misread or silently ignored.
- Crabbox: default AWS macOS runner requests to on-demand capacity so EC2 Mac proof commands do not fail on the unsupported Spot market default.
- Tests: run upgrade-survivor config recipe commands through the Windows npm shim so native Windows package walks keep baseline config coverage.
- Image tool: use bundled Anthropic media limits when resolving image compression policy without provider-runtime hooks.
- Tests: fail the kitchen-sink RPC Docker walk when gateway RSS sampling is unavailable instead of silently disabling the per-process memory guard.
- Tests: suppress the current Rolldown plugin timing warning format in the Vitest wrapper so tiny focused runs do not drown useful stderr in repeated build-timing noise.
- Models/OpenRouter: use endpoint-specific OpenRouter context limits from `top_provider` metadata so provider-routed models no longer overstate available context. (#85949) Thanks @TurboTheTurtle.
- Crabbox: sync clean sparse-checkout remote changed gates from a temporary full checkout with local-only commits overlaid as worktree changes so git-backed script checks can seed the runner repository.
- Agents: avoid loading bundled channel plugins while resolving completion delivery policy and queue defaults on subagent handoff paths.
- Tests: allow split Vitest config shards through the explicit-target preflight so CI shard jobs run their intended projects.
- Tests: make startup memory and startup bench smoke scripts build CLI startup artifacts when run from a fresh source checkout.
- iMessage: mark authorized slash-command turns as text-sourced commands so `/status`, `/new`, and `/restart` acknowledgements return to the source conversation. (#82642) thanks @homer-byte.
- Crabbox: install Corepack shims into the writable hydration `PNPM_HOME` so local AWS runner hydration no longer tries to overwrite `/usr/local/bin/pnpm`.
- Live tests: fail Gateway live model sweeps when selected coverage is lost to timeouts or stale high-signal filters instead of reporting false missing-profile coverage, and pin Docker OpenAI gateway coverage to the current `gpt-5.5` lane.
- Tests: fail Docker resource-ceiling checks when stats samples or configured limits are invalid instead of silently reporting zero peaks.
- Agents: fail closed when provider-less session models match multiple provider-prefixed runtime policies so CLI runtime routing no longer depends on config order. (#85970) Thanks @potterdigital.
## 2026.5.24
### Changes
- iMessage: support thumb-approval reactions — `👍` (Like tapback) resolves an approval as `allow-once` and `👎` resolves as `deny`, with the explicit-approver allowlist read from `channels.imessage.allowFrom`; `allow-always` stays on the manual `/approve <id> allow-always` text fallback. Mirrors the WhatsApp behavior from #85477.
- Gateway/perf: reuse process-stable channel catalog reads, avoid repeated bundled-channel boundary checks, and rotate gateway watch CPU profiles so benchmark runs do not accumulate unbounded artifacts.
- Gateway/perf: cache stable install-record, channel-catalog, bundled-channel, and Telegram session-store metadata during process-local hot paths to reduce repeated JSON and manifest reads.
- Gateway/perf: reuse immutable plugin metadata snapshots across startup, config, model, channel, setup, and secret metadata readers so hot paths avoid repeated plugin file stats and manifest registry reloads.
- Talk/realtime: let WebUI and Discord voice callers ask for active OpenClaw run status, cancel, steer, or queue follow-up work while a consult is still running. (#84231) Thanks @Solvely-Colin.
- Discord/voice: add realtime wake-name gating with agent-name defaults and raise profile bootstrap context budget for longer `USER.md`/`SOUL.md` files.
- Gateway/perf: lazy-load startup-idle plugin work, core gateway method handlers, and the embedded ACPX runtime so Gateway health and ready signals no longer wait on unused handler trees or ACPX probes.
- Gateway/perf: cache plugin SDK public-surface alias maps and skip irrelevant macOS Linuxbrew PATH probes so Gateway startup avoids repeated filesystem walks and slow missing-directory stats.
- Image tool: add adaptive model-aware image compression with an `agents.defaults.imageQuality` preference for choosing token-efficient, balanced, or high-detail media handling.
- Meeting Notes: add a source-only external meeting-notes plugin and SDK source-provider contract outside the core npm package, with auto-start capture config, manual transcript imports, read-only `openclaw meeting-notes` CLI access, and Discord voice as the first live source.
- Meeting Notes/Discord: release channel account startup before meeting-notes auto-capture, wait for the Discord voice manager during gateway boot, and stop plugin services before channel shutdown so voice capture state remains available during startup and cleanup.
- Docs/channels/config: add Signal `configPath`, Telegram wildcard topic defaults, local-time backup archive names, Termux home fallback, include-path validation, secret-scanner-safe placeholder guidance, Gemini CLI/Antigravity media guidance, and macOS VM auto-login guidance. Thanks @NorseGaud, @yudistiraashadi, @huangqian8, @VibhorGautam, @maweibin, @tianxingleo, @IgnacioPro, and @xzcxzcyy-claw.
- Docs: clarify model-usage portability, Codex migration prerequisites, status bootstrap wording, thread-bound subagent limits, hook ownership, and config-preserving safety guidance. Thanks @aniruddhaadak80, @leno23, @TomDjerry, @matthewxmurphy, @vincentkoc, and @stablegenius49.
- Docs: clarify README onboarding and Gateway startup paths, WhatsApp QR/408 recovery, cron output language prompts, skill advanced features, gateway upstream 403 troubleshooting, and plugin fallback override guidance. Thanks @deepujain, @Zacxxx, @Jah-yee, @neyric, @usimic, @Renu-Cybe, @BigUncle, and @SeashoreShi.
- Docs: clarify context-pruning ratio bounds, local dashboard recovery, CLI env markers, remote onboarding token behavior, and Peekaboo Bridge permissions for subprocess agents. Thanks @ayesha-aziz123, @dishraters, @hougangdev, and @brandonlipman.
- Docs: clarify browser CDP diagnostics, Plugin SDK allowlist imports, status-reaction timing defaults, queue steering behavior, limited-tool troubleshooting, cron HEARTBEAT handling, Telegram multi-agent groups, Bitwarden SecretRef setup, and EasyRunner deployments. Thanks @Quratulain-bilal, @mbelinky, @Mickey-, @vancece, @xenouzik, @posigit, @surlymochan, @janaka, and @choiking.
- CLI/models: let `openclaw models auth login` store a single returned provider auth profile under a requested `--profile-id`, and document named Codex OAuth profile setup. (#49315) Thanks @DanielLSM.
- Crabbox/Testbox: run clean sparse-checkout Testbox syncs from a temporary full checkout and route remote changed gates through Corepack pnpm.
- Docs: clarify IPv4-only Gateway BYOH binding, trusted-proxy scope clearing, Android pairing approval, macOS Accessibility grants, Zalo profile env vars, password-store SecretRef setup, and Chinese memory navigation. Thanks @itskai-dev, @gwh7078, @longstoryscott, @MoeJaberr, and @yuaiccc.
- Docs: consolidate GLM under Z.AI, add the Upstash Box install guide and Gateway exposure runbook, clarify MEDIA directives, Copilot and Voyage setup, config path quoting, real behavior proof, and memory-file write guidance. Thanks @BobDu, @alitariksahin, @Jefsky, @musaabhasan, @OmerZeyveli, @leno23, @WuKongAI-CMU, @luoyanglang, and @majin1102.
- Docs: clarify media provider credentials, Codex/OpenClaw code-mode boundaries, Slack and Telegram ack reactions, Feishu dynamic agents, secrets plaintext boundaries, memory guidance, and Chinese glossary terms. Thanks @nielskaspers, @cosmopolitan033, @drclaw-iq, @alexgduarte, @zccyman, @chengoak, and @cassthebandit.
- Packaging: exclude documentation images and assets from the npm tarball, reducing published package size without affecting runtime docs search or CLI behavior. Thanks @SebTardif.
- Media understanding: stop auto-probing Gemini CLI and use Antigravity CLI only as a lower-priority image/video fallback after configured provider APIs.
- Diagnostics: emit sanitized `secrets.prepare` timeline spans for Gateway secret preparation so operators can distinguish secret startup latency without exposing provider names, secret ids, or secret values. (#83019) Thanks @samzong.
- Diagnostics: export bounded skill usage metrics/spans and tool source/owner labels for core, plugin, MCP, and channel tool execution without exposing raw paths or session identifiers. (#80370) Thanks @gauravprasadgp.
- Agents/subagents: limit default sub-agent bootstrap context to `AGENTS.md` and `TOOLS.md`, keeping persona, identity, user, memory, heartbeat, and setup files out of delegated workers by default. (#85283) Thanks @100yenadmin.
- Maintainer skills: require clean autoreview before surfacing bug-sweep PR URLs and treat changelog-only conflicts as routine busy-main churn.
- Maintainer skills: exclude plugin SDK/API boundary work from `openclaw-landable-bug-sweep` so bugbash sweeps stay focused on small paper-cut fixes.
- QA-Lab/diagnostics: extend the OpenTelemetry smoke harness to prove trace, metric, and log export, and add first-class Prometheus and observability smoke aliases.
- Plugin SDK: add a generic channel-message poll sender so channel plugins can expose poll delivery without depending on channel-specific SDK facades.
- Plugin SDK/cron delivery: route cron delivery through the modern target resolver and outbound session-route APIs, deprecate parser-backed target helpers and `plugin-sdk/messaging-targets`, and move bundled callers to `plugin-sdk/channel-targets`.
- Crabbox: keep the local wrapper's provider validation synced with the installed Crabbox binary while preserving supported aliases such as `docker` and `blacksmith`. (#85302) Thanks @hxy91819.
- Maintainer skills: add `openclaw-landable-bug-sweep` for producing five small, reviewed, CI-green OpenClaw bugfix PRs from issue/PR sweeps.
- Control UI/chat: add search and Load More pagination to the chat session picker, keeping initial session loads bounded while making older conversations reachable. (#85237) Thanks @amknight.
- CLI/onboarding: start classic onboarding when bare `openclaw` runs before an authored config exists, while keeping configured installs on Crestodian. (#72343) Thanks @fuller-stack-dev.
- Discord: allow configuring a bounded `agentComponents.ttlMs` callback registry lifetime for long-running component workflows, with per-account overrides and a 24-hour cap. (#84189) Thanks @100menotu001.
- xAI/Grok: reuse xAI OAuth auth profiles for Grok `web_search`, thread active-agent auth through web search, add Grok model aliases, and let media providers declare default operation timeouts. (#85182) Thanks @fuller-stack-dev.
- Plugin SDK: add row-level session workflow helpers and deprecate `loadSessionStore` so plugins can read and patch sessions without depending on the legacy whole-store shape. (#84693) Thanks @efpiva.
- Gateway/plugins: reuse a compatible Gateway startup plugin registry during dispatch so safe plugin dispatches avoid redundant registry loading. (#84324) Thanks @ai-hpc.
- Plugins/SDK: add a general `embeddingProviders` capability contract and registration API so embeddings can become a reusable provider surface outside memory-specific adapters.
- Dependencies: refresh provider, plugin, UI, and tooling packages, update `protobufjs` to 8.4.0 to clear the current npm advisory, and carry the Claude ACP completion patch forward to `@agentclientprotocol/claude-agent-acp` 0.36.1.
- Agents/tools: remove the old sender-owner tool gating path so configured tools stay visible for trusted sessions while command and channel-action auth still carry real sender identity.
- QA-Lab: add curated mock JSONL replay fixtures and first-drift reporting for runtime-parity audits. (#80323, refs #80176) Thanks @100yenadmin.
- QA-Lab: add a QA bus tool-trace visibility scenario for sanitized tool-call assertions.
- QA-Lab: replace generic evidence framing in seeded scenario prompts with concrete observed QA behavior.
- QA-Lab: list named scenario packs in the coverage report so personal-agent privacy coverage stays visible in audits.
- QA-Lab: list live transport lane membership in the coverage report so real transport checks stay separate from seeded qa-channel scenarios.
- Release/package: run package integrity checks before package acceptance lanes so public install/update validation fails before private QA assets can leak into the package.
- QA-Lab: include the optional 100-turn runtime parity soak in release-soak artifacts so long-run Codex/Pi transcript drift stays visible outside the default gate. (#80395) Thanks @100yenadmin.
- QA-Lab: add a live-only long-context progress watchdog scenario for Codex app-server timeout and stalled-run sentinels. (#80323) Thanks @100yenadmin.
- QA-Lab: tag gateway restart recovery and streaming final-integrity scenarios as live-only runtime parity lanes. (#80323) Thanks @100yenadmin.
@@ -20,21 +126,236 @@ Docs: https://docs.openclaw.ai
- QA-Lab: include an opt-in `update.run` package self-upgrade sentinel for destructive latest-package recovery checks.
- QA-Lab: add Codex plugin lifecycle and auth-profile fixture coverage for missing installs, pinned-version drift, first-turn install ordering, and doctor migration safety. (#80323, refs #80174) Thanks @100yenadmin.
- Models/perf: pre-warm the provider auth-state map at gateway startup so `/models` and every model-listing call short-circuits the per-provider plugin / external-CLI discovery on the hot path. Per-call cost drops from ~20 s to ~5 ms (~4,100×); the one-time startup warm resets and re-warms after hot reloads. (#84816) Thanks @sjf.
- Release/security: ship the root npm package and OpenClaw-owned npm plugins with generated shrinkwrap, support bundled plugin runtime dependencies for suitable plugin tarballs, and require review for lockfile/shrinkwrap changes so published installs use locked dependency graphs.
- Tests/perf: isolate doctor core health check unit coverage from real skills/workspace discovery so `doctor-core-checks` no longer dominates unit perf while keeping one real skills-readiness smoke. (#84493) Thanks @frankekn.
### Fixes
- Gateway/update: avoid fetching unrelated tags during dev-channel git updates so moved release tags do not block branch-based updates. (#84737) Thanks @rubencu.
- CLI/update: suppress the expected future-config warning while an old update parent hands off to the freshly installed post-core process.
- MiniMax: store OAuth token expiry as an absolute millisecond timestamp so OAuth profiles no longer appear expired on every request. (#83480) Thanks @NianJiuZst.
- Agents/Anthropic: strip missing or blank thinking signatures for signed-thinking providers even when recovery supplies a narrow replay policy without signature preservation. Fixes #84430. (#84448) Thanks @NianJiuZst.
- Agents/channels: send a visible notice when an aborted main session cannot be resumed after restart, including Telegram group targets. (#85805) Thanks @pfrederiksen.
- Discord/voice: serialize overlapping voice joins, retry aborted startup readiness within the configured timeout, upgrade meeting-notes-only sessions to realtime when the normal follow join arrives, detach promoted meeting-notes ownership without leaving voice, and include `OpenClaw` in default realtime wake names.
- Gateway/restart: honor the configured restart drain budget for embedded runs and avoid spending the deferral timeout twice after forced restart timeouts. (#85708) Thanks @Kaspre.
- Gateway/boot: run `BOOT.md` startup checks in an isolated boot session so gateway restarts do not overwrite the agent's main session mapping. (#85479)
- Meeting Notes: include a speaker-labeled transcript section in generated summaries so Discord group voice captures show who said each captured utterance.
- Discord/voice: recover stale realtime playback state when Discord stream-close/player-idle events do not arrive, and keep generated runtime plugin aliases available after postbuild rewrites.
- Discord/voice: keep realtime playback running when meeting notes attaches to an existing voice session or a realtime consult starts, and route realtime user transcripts into meeting notes.
- Config/secrets: preflight active runtime SecretRefs before root and include config writes persist, and roll back unchanged file/env state when post-write refresh fails. Fixes #46531. (#84454) Thanks @samzong.
- CLI/models: preserve SecretRef-backed custom provider `apiKey` markers when `models status` regenerates `models.json`, avoiding resolved plaintext secrets on disk. Fixes #84632. (#84658) Thanks @NianJiuZst.
- WhatsApp/auto-reply: deliver deferred media replies through the foreground reply fence so overlapping no-reply turns no longer hide already visible responses. (#85517) Thanks @cavit99.
- Sessions/security: replace agent-to-agent wildcard allowlist regexes with a precompiled linear matcher so cross-agent access checks avoid backtracking-prone patterns. (#85849) Thanks @SebTardif.
- WebChat: keep the run-complete indicator in progress until deferred history replay renders the assistant reply, so Done no longer appears before response text. (#85374) Thanks @neeravmakwana.
- Agents/tools: give timed-out or cancelled process trees a bounded SIGTERM cleanup window before SIGKILL while preserving tree-aware cancellation. Fixes #66399. (#85865) Thanks @IWhatsskill.
- Agents/subagents: treat aborted subagent stop reasons as killed terminal failures so parent sessions get error announcements instead of silent success. Fixes #72293. (#85860) Thanks @IWhatsskill.
- Agents/providers: clamp proxy-like OpenAI Chat Completions output caps against the final request payload so strict local/API-compatible servers no longer reject prompts that already consume part of the context window. Fixes #83086. (#85889) Thanks @rendrag-git.
- Agents/compaction: skip agent-harness preflight for provider-owned CLI runtime sessions so over-threshold Claude CLI sessions continue through normal compaction instead of failing on a missing harness. Fixes #84857. (#84878) Thanks @zhangguiping-xydt.
- Codex/app-server: keep successful native hook relays available through a short post-turn grace window so late Codex hook subprocesses can finish policy enforcement without clearing a replacement relay. (#83987) Thanks @Kaspre.
- Control UI/config: save form-mode edits from the source config snapshot so runtime-only provider defaults like empty `models.providers.<id>.baseUrl` are not written back and rejected. Fixes #85831. Thanks @garyd9.
- Browser/existing-session: launch Chrome DevTools MCP with usage statistics disabled by default so its telemetry watchdog stays off unless an operator explicitly opts in. (#85886) Thanks @rohitjavvadi.
- Telegram: normalize legacy durable group retry targets before retry sends, polls, and pins so group retries keep using the real chat id. (#85656) Thanks @luoyanglang.
- Agents/PDF: route MiniMax PDF fallback policy through plugin metadata so MiniMax uses text extraction instead of VLM image fallback. (#85590, fixes #85575) Thanks @neeravmakwana.
- CLI/plugins: tighten timeout, numeric option, media payload, permission, profile/TLS, plugin metadata, JSON, and remote URL handling; prevent stuck progress/app-server/IRC/Synology/Twitch waits; and keep imported chat history ordering stable.
- Telegram/config: suppress the missing `accounts.default` warning when `channels.telegram.defaultAccount` names a configured account that also sorts first. Fixes #83948. Thanks @crypto86m.
- Telegram: serialize visible topic replies through core reply-lane admission so heartbeat and queued follow-up turns cannot continue ownerless or misroute responses. (#85709) Thanks @jalehman.
- CLI/node: print node status recovery hints on stdout consistently while keeping status errors on stderr. Fixes #83925. Thanks @davinci282828.
- WebChat: summarize internal message-tool source replies so tool cards no longer duplicate the visible reply body. (#84773) Thanks @jason-allen-oneal.
- Gateway/WebChat: hide duplicate `gateway-injected` assistant rows when Cursor ACP already persisted the same `acp-runtime` reply. Fixes #85741. Thanks @lxf-lxf.
- WebChat: scope the visible attachment button to its own composer file input so clicking Upload reliably opens the file picker. (#83952, fixes #47983) Thanks @jason-allen-oneal.
- Gateway: preserve deferred lifecycle-error cleanup across later non-terminal events so provider timeouts can persist failed session state instead of leaving sessions stuck running. (#85256, fixes #63819) Thanks @samzong.
- Gateway/update: stop treating inherited macOS `XPC_SERVICE_NAME` values as launchd supervision during update respawn, so GUI-spawned gateways use detached respawn instead of exiting for a missing LaunchAgent. Fixes #85224. Thanks @richardmqq.
- Gateway: stop sending duplicate message-phase `sessions.changed` websocket events after displayable `session.message` transcript updates. (#84834)
- Agents/subagents: report tool-only child progress during timeout summaries instead of showing no visible output.
- Telegram/ACP: preserve explicit `:topic:` conversation suffixes when inbound ACP targets do not carry a separate thread id.
- Browser/proxy: bypass the managed proxy for the exact local managed Chrome CDP readiness and DevTools WebSocket endpoints, so `openclaw browser start` works when the operator proxy blocks loopback egress. (#83255) Thanks @lightcap.
- Ollama: bypass the managed proxy for configured local embedding origins while keeping SSRF guardrails on unconfigured targets. Thanks @Kaspre.
- OpenAI/images: route Codex API-key image generation through the native OpenAI Images API instead of the Codex OAuth streaming backend, avoiding 401s from valid API keys.
- Agents/OpenAI completions: omit empty tool payload fields for proxy-like OpenAI-compatible endpoints so strict vLLM-style servers accept tool-free turns. (#85835) Thanks @rendrag-git.
- Sandbox: keep workspace skill mounts read-only for remote container-cwd file operations and reject symlinked skill roots before creating protected overlays. (#85591) Thanks @jason-allen-oneal.
- Scripts/Windows: route remaining QA, release, profile, and live-media `pnpm` launches through the managed runner so native Windows avoids brittle `.cmd` execution and shell-argv warnings.
- Release: align generated config/API baselines and the meeting-notes plugin version so release preflight stays green on native Windows.
- Install/Windows: run Git hook setup through a Node prepare helper so native Windows installs no longer print POSIX shell errors.
- Checks/Windows: chunk and serialize extension oxlint shards on native Windows so changed gates avoid Go-backed linter memory spikes.
- Release/Windows: run installed `openclaw.cmd` verification through explicit `cmd.exe` wrapping so npm prepublish/postpublish checks avoid Node shell-argv warnings.
- Release/Windows: run release-check npm pack/install/root probes through the shared npm runner so native Windows avoids bare `npm` lookup and `.cmd` shell-argv handling.
- Release/Windows: run cross-OS release check `.cmd` shims through explicit `cmd.exe` wrapping so native Windows install and gateway probes avoid Node shell-argv handling.
- Control UI/Windows: run i18n Pi, npm, and pnpm helper commands through explicit Windows runners so native Windows translation sync avoids brittle `.cmd` launches.
- Scripts/Windows: run the Z.AI fallback repro through the shared pnpm runner so native Windows avoids raw `.cmd` launches.
- Codex/Windows: run app-server protocol formatting through the shared pnpm runner so native Windows avoids raw `.cmd` launches.
- Plugins/Windows: run plugin npm package staging through the shared npm runner so native Windows release checks avoid bare `npm` lookup and `.cmd` shell-argv handling.
- Checks/Windows: route full `pnpm check` stage commands through the managed child runner so Windows avoids Node shell-argv deprecation warnings there too.
- Agents/fs: allow workspace-only host write/edit tools to write through in-workspace symlink directory parents while preserving outside-workspace symlink rejection. Fixes #84696. Thanks @garbagenetwork.
- Checks/Windows: run managed child commands through explicit `cmd.exe` wrapping instead of Node shell mode with argv, avoiding Node 24 subprocess deprecation warnings during changed checks.
- Gateway: omit internal stream-error placeholder entries from agent prompt history so failed assistant turns are not replayed as model-authored text. (#85652) Thanks @anyech.
- Sessions: enforce the session write-lock max-hold policy during lock acquisition so long-held locks can be reclaimed before the stale-lock window. (#85764) Thanks @njuboy11.
- Sessions/status: preserve user-facing model, fallback, usage, and cost attribution when internal subagent handoff runs use fallback models. (#85726, fixes #85082) Thanks @brokemac79.
- Install/update: honor `OPENCLAW_HOME` when deriving default dev checkout and installer onboarding paths, while keeping explicit `OPENCLAW_GIT_DIR` and `OPENCLAW_CONFIG_PATH` overrides authoritative. Fixes #54014. Thanks @robertPiro.
- Models: prune retired Groq, GitHub Copilot, OpenAI, xAI, and old Claude catalog entries, with doctor migration to upgrade existing configs to current provider refs.
- Plugins/Gateway: treat non-empty return values from plugin gateway method handlers as successful responses so `openclaw gateway call` no longer times out after completed plugin work. Fixes #59470. Thanks @HTMG23.
- Doctor/update: recognize junction-backed source checkouts as git installs by comparing canonical paths before showing package-manager update guidance. Fixes #82215. Thanks @igormf.
- Channels: honor `/verbose on` for tool/progress summaries across direct chats, groups, channels, and forum topics while preserving quiet default behavior. (#85488) Thanks @kurplunkin.
- Update: keep the detached gateway restart handoff best-effort when the restart script process cannot be spawned. (#83892) Thanks @davinci282828.
- Windows/config: skip POSIX login-shell env fallback on native Windows so startup no longer warns about missing `/bin/sh`. Fixes #84795. Thanks @JIRBOY.
- Telegram: persist the prompt-context message cache through plugin state and record bot-authored replies after sends and draft streaming so later turns can include prior assistant replies without relying on the JSON sidecar. (#85231) Thanks @keshavbotagent.
- Agents/subagents: keep Codex persona and user workspace files turn-scoped so native Codex subagents inherit only shared tool guidance by default. (#85811) Thanks @lastguru-net.
- CLI/skills: show an all-ready note with next-step commands when skill setup has no missing dependencies to install. (#85032) Thanks @aniruddhaadak80.
- Microsoft Foundry: route DeepSeek V4 Pro and Flash models through the Foundry Responses API while keeping older DeepSeek models on their existing path. (#85549) Thanks @roslinmahmud.
- Status/usage: show configured cost estimates for AWS SDK models in full usage output while keeping token-only usage replies cost-free. (#85619) Thanks @ItsOtherMauridian.
- Agents/OpenAI Responses: retry non-visible reasoning-only turns for OpenAI Responses API families instead of treating them as empty failed turns. (#85603) Thanks @SebTardif.
- Directive tags: preserve message and content-part object identity when display stripping makes no directive-tag changes. (#85682) Thanks @willamhou.
- Telegram: send local `path`/`filePath` and structured attachment media from `sendMessage` actions instead of dropping them or sending text-only messages. (#85219) Thanks @keshavbotagent.
- Sessions/status: show the estimated context budget when fresh provider usage is unavailable and clear stale estimates across session resets and compaction boundaries. (#84830) Thanks @giodl73-repo.
- Gateway/config: pin relative `OPENCLAW_STATE_DIR` overrides to an absolute path at startup so later working-directory changes cannot retarget gateway state. (#52264) Thanks @PerfectPan.
- Checks/Parallels: make changed-lane scripts, shrinkwrap generation, and Parallels package smoke host commands run through native Windows-safe paths and `npm`/`pnpm` shims.
- Release/package: run npm release, prepublish, and postpublish verification through Windows-safe npm command shims so native Windows checks can execute `npm.cmd` instead of treating it as a binary.
- Agents/harness: pass CLI runtime aliases through harness selection so provider-owned CLI aliases no longer get rejected before reaching the right runtime. (#85631) Thanks @potterdigital.
- Secrets: show the irreversible apply warning after interactive `secrets configure` confirmation so confirmed migrations still get the final safety prompt. (#85638) Thanks @alkor2000.
- Agents/CLI output: ignore cumulative Claude `stream-json` result usage when assistant usage events are present, preventing inflated cache-read accounting. (#85625) Thanks @zhouhe-xydt.
- CLI: keep `waitForever()` alive by leaving its keep-alive interval ref'd so the public helper no longer exits immediately with Node's unsettled-await code. (#85694) Thanks @m1qaweb.
- Agents/bootstrap: guard bootstrap name checks against missing file names so malformed bootstrap entries warn and truncate instead of crashing. Fixes #85523. (#85615) Thanks @zhouhe-xydt.
- CLI/tasks: reject partially numeric `openclaw tasks audit --limit` values so audit limits must be real positive integers instead of accepting strings like `5abc`. (#84901) Thanks @jbetala7.
- Status/diagnostics: bound deep Docker audit probes so `openclaw status --deep` reports slow container checks instead of hanging behind unbounded inspection. (#85476) Thanks @giodl73-repo.
- Providers/Anthropic: migrate 1M context handling to GA-capable Claude 4.x models by sizing eligible models at 1M without the retired `context-1m-2025-08-07` beta, ignoring that retired beta in older configs, and preserving OAuth-required Anthropic beta headers. (#45613) Thanks @haoyu-haoyu.
- Cron/Telegram: parse forum-topic delivery targets through the Telegram plugin instead of cron core, including `:topic:` and `:topicId` forms for announce delivery. Thanks @etticat.
- Twitch: keep stale message-handler cleanup callbacks from removing newer handler registrations for the same account, preserving inbound message delivery after reconnects. Fixes #83888. (#85425) Thanks @alkor2000.
- Control UI/chat: keep light-mode model, thinking, config, and agents select arrows visible without tiling background icons. Fixes #85713. Thanks @Linux2010.
- Memory/LanceDB: expose public memory artifacts through the active memory provider bridge so memory-wiki imports durable memory files, daily notes, dream reports, and event logs without depending on memory-core internals. Fixes #83604. (#85060) Thanks @brokemac79.
- Crabbox: keep AWS hydration compatible with local Actions replay by inlining the hydrate workflow's Node/pnpm setup instead of invoking repo-local composite actions.
- Agents/subagents: simplify native sub-agent completion handoff so children report their latest visible assistant result to the requester without using `message`, while keeping parent-owned message-tool delivery policy intact. Fixes #85070. (#85089) Thanks @brokemac79.
- Docker setup: stop printing the Gateway bearer token in setup logs and printed follow-up commands.
- Gateway: defer channel account startup work until HTTP readiness and remove startup model prewarm, avoiding startup event-loop stalls and timer-delay warnings.
- Models/perf: reuse plugin metadata during models.json planning, keep bundled catalog augmentation manifest/static, and use static provider catalogs for metadata-only startup discovery so provider model normalization, auth discovery, and Gateway startup metadata do not reload broad plugin runtimes.
- Agents: let embedded compaction fallback retries proceed when PI-compatible candidates do not need agent harness plugin preparation.
- Agents/tools: honor configured custom provider API keys when deciding whether media, image-generation, video-generation, music-generation, and PDF tools are available. (#85570)
- StepFun: stop advertising stale generic API key auth choices so onboarding only offers runtime-backed Standard and Step Plan choices.
- Diagnostics: keep OpenTelemetry log bodies behind explicit content capture and scrub scoped agent-session keys from OpenTelemetry and Prometheus labels while preserving bounded queue-lane prefixes.
- Windows installer: fail Git checkout installs when `pnpm install` or `pnpm build` fails instead of writing a wrapper to a missing CLI build.
- Sessions: surface previous-transcript archive failures during `/new` rotation so disk rename errors are logged instead of silently hiding stranded transcript files. Fixes #81984. (#85586, from #82081) Thanks @0xghost42.
- TUI/agents: mirror internal-ui message-tool replies into final chat output so message-tool-only agents remain visible in `openclaw tui`. Fixes #85538. Thanks @danpolasek.
- Gateway/TUI: preserve source-reply metadata through reply normalization and emit message-tool-only agent replies over the live chat stream so `openclaw tui` renders Codex replies without waiting for a history refresh. Thanks @shakkernerd.
- Codex/TUI: keep long source-reply runs alive after Codex reasoning completes so delayed visible `message` calls can still reach `openclaw tui`. Thanks @shakkernerd.
- TUI: keep quiet active runs busy after the response watchdog notice instead of reopening the prompt and encouraging duplicate submissions while the backend turn is still running. Thanks @shakkernerd.
- Agents: preserve the latest assistant thinking blocks while stripping invalid replay signatures from older turns, and retry Anthropic thinking failures without thinking replay. Fixes #85557. Thanks @bryanbaer.
- Agents: keep parallel OpenAI-compatible tool-call deltas in separate argument buffers so interleaved tool calls no longer corrupt streamed arguments. (#82263) Thanks @luna-system.
- Telegram: avoid false pairing prompts after transient pairing-store read failures while preserving configured `allowFrom` and per-DM pairing authorization. (#85555)
- Memory/doctor: report missing or unusable QMD workspace directories as workspace failures instead of generic binary failures. (#63167) Thanks @sercada.
- Debug proxy: record CONNECT client-socket errors and destroy the paired upstream socket so abrupt client disconnects no longer leak tunnel resources. (#82444) Thanks @SebTardif.
- Diffs: continue hydrating later diff cards when one card fails so a single broken card no longer blanks the whole diff viewer. (#84775) Thanks @cosmopolitan033.
- Mac app: use the native settings sidebar window chrome so the sidebar toggle stays on the left and content no longer clips under oversized titlebar padding.
- QA-Lab/Codex: bundle auth/plugin fixture imports for flow scenarios and let terminal async media tools end Codex app-server turns without timing out. (#80397, refs #80323) Thanks @100yenadmin.
- WhatsApp: persist inbound message delivery state through plugin state before dispatch and delay read receipts until handler completion, so retryable failures can redeliver without adding a plugin-local disk cache. Thanks @samzong.
- Gateway/agents: preserve fresh session overrides and metadata when stale cached agent-session entries race with store updates, so subagent model/provider overrides and routing policy survive concurrent writes. (#19328) Thanks @CodeReclaimers.
- Control UI/chat: keep chat session search inline with the session selector so the header no longer shows a duplicate standalone search row.
- Control UI/chat: collapse focused-mode header chrome and suppress hidden-header scroll updates so focus mode no longer jumps while scrolling. Thanks @amknight.
- Codex app-server: restart the native app-server and retry once when server-side compaction times out, so preflight compaction stalls recover instead of failing every dispatch. (#85500)
- Restore Control UI gateway token pairing [AI]. (#85459) Thanks @pgondhi987.
- OpenAI video: honor configured provider request private-network opt-in for local/custom video endpoints so explicitly trusted mock and self-hosted providers are not blocked. Thanks @shakkernerd.
- OpenAI video: send uploaded video edit requests to the documented `/videos/edits` endpoint with a `video` file instead of posting MP4 references to `/videos`. Thanks @shakkernerd.
- Agents/channels: preserve message-tool delivery evidence through gateway agent completion handoffs so successful generated media sends are not followed by false failure messages. Thanks @shakkernerd.
- CLI/update: repair managed npm plugin `openclaw` peer links during post-core convergence and reject stale or wrong-target peer links before restart. (#83794) Thanks @fuller-stack-dev.
- CLI/agents: default new omitted-account bindings to all accounts when the channel has multiple configured accounts, and clarify account-scope docs. (#49769) Thanks @Gcaufy.
- Codex app-server: let authorized `/codex` control commands such as `/codex detach` escape plugin-owned conversation bindings while keeping unknown or unauthorized slash text routed to the bound plugin. Fixes #85157. (#85188) Thanks @TurboTheTurtle.
- Auto-reply/models: keep `/models` browse replies fast by sharing the bounded read-only catalog path with Gateway model listing. (#84735) Thanks @safrano9999.
- Browser/Doctor: read macOS Chrome app bundle versions from `Info.plist` before spawning Chrome and extend the fallback version probe timeout, avoiding false cold-cache warnings from Gatekeeper latency. Fixes #85418. Thanks @davidcittadini.
- Codex app-server: disable native Code Mode when the effective exec host is `node` and keep OpenClaw `exec`/`process` available, so `/exec host=node` routes shell commands through the selected node instead of the gateway. Fixes #85012. (#85090) Thanks @sahilsatralkar.
- Agents: bound embedded auto-compaction session write-lock watchdogs to the compaction timeout instead of the full run timeout, so stuck compaction cannot hold the live session lock for the whole run window. (#84949) Thanks @luoyanglang.
- Gateway/agents: return phase-aware `agent.wait` timeout attribution and only cool auth profiles on provider-started timeouts. Refs #65504. Thanks @100yenadmin.
- Gateway/systemd: launch managed update handoff helpers in a transient user scope so systemd-supervised Update Now flows survive the gateway unit restart. Fixes #84068.
- Gateway: defer provider auth-state prewarm until after startup readiness so early gateway tool/session requests are not blocked by provider auth discovery. (#85272) Thanks @dutifulbob.
- Gateway/models: coalesce provider auth-state rewarms after auth-profile failures and log event-loop delay for warm/rewarm work, so provider auth bursts no longer stack full auth sweeps behind channel replies.
- Gateway/models: stop cancelled provider auth-state prewarms from continuing full provider sweeps, so reload and auth-failure bursts no longer keep startup busy.
- Agents/Codex: show the first plan update as a transient chat status notice without counting it as final assistant content.
- CLI/update: walk the macOS process ancestry and honor the inherited Gateway runtime PID before package updates stop the managed Gateway service, so nested in-band updater children can refuse instead of killing the LaunchAgent-supervised Gateway that owns them. Fixes #85120.
- Gateway/LaunchAgent: wait for launchd reload bootout to finish and fall back to kickstart when bootstrap races, so reload handoff does not leave the service deregistered. Fixes #84630. (#84641) Thanks @NianJiuZst.
- Gateway/LaunchAgent: treat a concurrent launchd bootstrap as a successful restart when the service is already loaded, avoiding false macOS Gateway restart failures. Fixes #84721. (#84722) Thanks @googlerest.
- Gateway/service: include the active `openclaw` command bin directory in managed service PATH generation and doctor audit expectations for npm-global macOS installs. Fixes #84201. (#84475) Thanks @jbetala7.
- Control UI/chat: disable the thinking selector for known non-reasoning models instead of showing duplicate Off choices. Fixes #84069. Thanks @DrippingMellow.
- Memory: expand `~` in configured extra memory paths before resolving them, so home-relative folders are not treated as workspace-relative. Fixes #58026. Thanks @stadman.
- Skills: treat `openclaw.os: macos` as Darwin when checking skill requirements, so macOS-only skills no longer report as missing on macOS hosts. Fixes #61338. Thanks @Jessecq1995.
- Control UI/logs: strip ANSI escape sequences from displayed Gateway log messages so color codes no longer appear as raw text. Fixes #64399. Thanks @guguangxin-eng.
- Docker: pre-create the workspace and auth-profile config mount points with `node` ownership so first-run named volumes do not start root-owned. Fixes #85076. Thanks @Noerr.
- Telegram: pass configured markdown table mode through outbound markdown chunking so chunked sends render tables consistently. Fixes #85085. Thanks @ShuaiHui.
- Diagnostics/OTel: drop snake_case diagnostic id attributes alongside camelCase ids so exported telemetry cannot leak run, session, message, chat, trace, or tool-call identifiers. (#72645) Thanks @Lion0710.
- CLI/update: preserve managed Gateway service environment during package cutovers so macOS LaunchAgent repair/restart reads the pre-update service state instead of caller shell state. (#83026)
- Agents/providers: honor per-model `api` and `baseUrl` overrides in custom provider auth hooks and transport selection. Fixes #80487. (#80488) Thanks @huveewomg.
- Gateway/restart: eager-load the lifecycle runtime before in-place upgrade signal handling so package replacement does not deadlock restart imports. (#84890) Thanks @myps6415.
- CLI/update: start managed Gateway update handoff helpers from a stable existing directory and tolerate deleted cwd/package roots during macOS LaunchAgent handoff. Fixes #83808. (#83875) Thanks @jason-allen-oneal.
- Skills: watch each shared skill directory once across agent workspaces instead of once per agent, preventing file-descriptor exhaustion (`EMFILE`) that disposed bundle-mcp processes and stalled sessions on multi-agent gateways. Fixes #84968. (#85130) Thanks @openperf.
- Release/security: keep generated npm shrinkwrap package versions inside the pnpm lock graph so published package locks cannot bypass pnpm dependency age and override policy.
- Cron: honor `cron.retry.retryOn: ["network"]` for common network error codes such as `EAI_AGAIN`, `EHOSTUNREACH`, and `ENETUNREACH`.
- Gateway chat: broadcast returned agent-run error payloads after an agent starts so ACP/WebChat clients receive terminal idle-timeout errors. Fixes #84945.
- Gateway chat display: preserve OpenAI-compatible `prompt_tokens`, `completion_tokens`, and `total_tokens` usage fields in sanitized chat history so llama.cpp sessions keep context counts. Fixes #77992. Thanks @MarTT79.
- Dashboard/CLI: allow macOS browser launching through `open` even when SSH environment variables are present, while preserving Linux SSH no-display protection. Fixes #67088. Thanks @theglove44.
- Codex app-server: keep native web search observations out of mirrored chat transcripts while preserving available action query metadata in tool progress telemetry. Fixes #85109. Thanks @ugitmebaby.
- OpenCode Go: strip unsupported Kimi reasoning replay fields before provider requests so repeated `kimi-k2.6` turns do not fail schema validation. Fixes #83812. Thanks @Sleeck.
- Browser/CDP: add a WSL2 portproxy self-loop hint when Chrome DevTools endpoints accept connections but return an empty HTTP reply. Fixes #59209. Thanks @Owlock.
- Agents/tools: add bounded tool-policy audit log entries that identify which allow/deny rule removed tools or blocked a sandboxed tool call. Fixes #55801. Thanks @justinjkline.
- CLI/logs: read implicit local Gateway logs through the passive backend client path so `openclaw logs --follow` does not register as a paired device, and use the active Linux systemd journal instead of stale configured-file fallbacks when live local RPC is unavailable. Fixes #83656 and #66841.
- Agents/OpenAI: preserve structured provider error code, type, and redacted body metadata on boundary-aware transport failures.
- Doctor/Codex: point native Codex asset warnings at the canonical `openclaw migrate plan codex` preview command. Fixes #84948. Thanks @markoa.
- CLI/models: make `capability model auth logout --agent` remove auth profiles from the selected non-default agent store. Fixes #85092. Thanks @islandpreneur007.
- Gateway/models: reuse prepared provider auth metadata during model-listing auth checks so repeated lookups avoid broad plugin discovery while preserving synthetic local auth.
- CLI/status: suppress systemd user-service setup hints when `openclaw status --deep` can already reach a running Gateway RPC service. Fixes #85094. Thanks @islandpreneur007.
- CLI/devices: recover local approval when a same-device repair request replaces the request ID being approved.
- CLI/agents: retry transient normal-close Gateway handshakes before falling back to embedded `openclaw agent` execution.
- CLI/update: keep managed Gateway service stop/restart status lines out of `openclaw update --json` stdout so package-update automation can parse the JSON payload.
- Plugins: resolve OpenClaw plugin SDK subpaths for native external plugin runtimes without mutating package installs or broadening process-wide module resolution.
- Agents/OpenAI: preserve Responses and Chat Completions `reasoning_tokens` usage metadata without double-counting it in aggregate output tokens. (#85319)
- Control UI/chat: convert pasted `data:image/...;base64,...` clipboard text into an image attachment instead of dumping the payload into the composer. Fixes #62604. Thanks @cpwilhelmi.
- Providers/Gemini: strip fractional seconds from web-search time range filters so Gemini accepts freshness-bound search requests. (#85071) Thanks @Noerr.
- OpenAI Codex: preserve image input support for sparse `openai-codex/gpt-5.5` catalog rows. (#85095) Thanks @sercada.
- CLI/models: add a piped or pasted API-key path for OpenAI Codex auth and warn when API keys are pasted into token-mode auth. (#85533) Thanks @joshavant.
- Telegram: dead-letter missing-harness isolated ingress failures so a poisoned spooled update no longer blocks later same-lane messages. Fixes #85470. (#85605) Thanks @joshavant.
- Plugins/discovery: strip `-plugin` package suffixes when deriving plugin id hints so package names line up with manifest ids. (#85170) Thanks @JulyanXu.
- Tlon: stop advertising a non-existent agent tool contract in the plugin manifest.
- Telegram: preserve fenced code block languages through Markdown rendering so Telegram receives `language-*` code classes. (#85209) Thanks @leno23.
- Windows installer: run npm and Corepack command shims from a Windows-local directory so installs launched from WSL2 UNC paths do not fail before OpenClaw is installed.
- Windows updates: roll back git-backed updates to the previous checkout when dependency install, build, UI build, or doctor repair fails.
- Windows installer: persist user-local portable Git on PATH and activate the repo-pinned pnpm version for git-backed installs and updates.
- Windows installer: bootstrap a user-local portable Node.js when native Windows has no Node and no winget, Chocolatey, or Scoop, so first-run installs can continue on raw hosts.
- Windows installer: extract the downloaded portable Node.js directory with native `tar` before falling back to .NET zip extraction, avoiding PowerShell 5.1 archive and path-length failures.
- fix(integrations): enforce channel read target allowlists [AI]. (#84982) Thanks @pgondhi987.
- Agents/heartbeat: route single-owner `session.dmScope=main` direct-message exec and cron event wakes back to the agent main session so async completions no longer strand context in orphan direct-DM queues. Fixes #71581. (#83743) Thanks @Kaspre.
- Agents/code-mode: expose outer code-mode `exec` source through the `command` hook alias with `toolKind`/`toolInputKind` discriminators so exec-shaped policies can distinguish code-mode cells. (#83483) Thanks @Kaspre.
- Agents/code mode: return structured timeout and runtime-unavailable error codes for known worker failures. Fixes #83389. (#83444) Thanks @Kaspre.
- QA-Lab: isolate multi-scenario suite workers when scenarios need startup config patches, preventing message-routing config from leaking into unrelated scenarios.
- QA-Lab: make the commitments heartbeat-target-none scenario request an immediate heartbeat instead of waiting for the next scheduled heartbeat.
- Codex/Plugin SDK: deliver Codex-native subagent completions through a generic harness task runtime so harness-backed plugins can mirror durable task lifecycle and completion delivery without Codex-specific SDK imports. (#83445) Thanks @bryanpearson.
- Gateway CLI: surface local post-challenge connect assembly failures immediately instead of waiting for the wrapper timeout. Fixes #68944. (#85253) Thanks @samzong.
- Messages: strip unsupported web-search citation control markers from outbound replies before they reach WebChat or external channels. Fixes #85193. (#85204) Thanks @neeravmakwana.
- Agents/exec: treat denied exec approvals as terminal instead of feeding them back into agent follow-up work, and recognize Chinese stop phrases in abort handling. Fixes #69386. (#85194) Thanks @samzong.
- CLI/agents: abort accepted Gateway-backed `openclaw agent` runs on SIGINT/SIGTERM so cron and supervisor timeouts do not leave remote agent work alive. Fixes #71710. (#84381) Thanks @Kaspre.
- Codex app-server: retry replay-safe stdio client-close turns once using structured failure metadata, while surfacing idle `turn/completed` timeouts instead of blindly replaying active shared-server turns. Thanks @VACInc.
- Codex app-server: reject command overrides that embed Node or package-manager arguments and point users to `appServer.args`, so Windows startup avoids shell parsing failures. (#84417) Thanks @TurboTheTurtle.
- Agents/Copilot: drop unsafe GitHub Copilot Responses reasoning replay items before send so Telegram direct sessions no longer fail on overlong replay IDs. Fixes #85197. (#85198) Thanks @galiniliev.
- UI: add accessible tooltips to the topbar color-mode buttons so System, Light, and Dark choices are labeled on hover and focus. (#85227) Thanks @amknight.
- fix: constrain Windows task script names [AI]. (#85064) Thanks @pgondhi987.
- Control UI: keep the chat session picker from hiding older or cross-agent configured conversations while preserving the bounded configured-agent refresh. (#85211) Thanks @amknight.
- Agents/Anthropic: preserve unsafe integer tool-call input values in streamed Anthropic tool-use JSON, preventing Discord-style IDs from being rounded before dispatch. Fixes #47229. (#83063) Thanks @leno23.
- Agents/Codex: estimate tool-heavy prompt pressure at the LLM boundary before provider submission, so persistent sessions compact before overflowing context windows. (#85541) Thanks @fuller-stack-dev and @joshavant.
- Agents/hooks: wait for local one-shot CLI and Codex `agent_end` plugin hooks before process cleanup so terminal observability flushes reliably. (#85007)
- Providers/Google: preserve Gemini 3 cron `thinkingDefault: "low"` when stale catalog metadata says `reasoning:false`, so scheduled runs keep provider-supported thinking instead of downgrading to off. (#85185) Thanks @neeravmakwana.
- CLI/agents: allow `openclaw agent --session-key` to target explicit session keys, including agent-scoped legacy keys. (#85121) Thanks @Kaspre.
- Auto-reply/ACP: wait for same-channel block reply delivery before starting tool work, while still honoring ACP dispatch aborts so stopped turns do not wait on slow channel sends. (#83722) Thanks @IWhatsskill.
- Codex/ACP: mark required child-run completions that only report progress, omit a final deliverable, or fail requester delivery as blocked while preserving real final reports. (#85110) Thanks @IWhatsskill.
- Channels: treat bare abort messages such as `stop`, `abort`, and `wait` as immediate control commands in inbound debounce paths so stop requests are not delayed behind pending message coalescing. (#83348) Thanks @IWhatsskill.
- Channels/message tool: resolve configured external channel plugins during in-agent channel selection, so `openclaw agent --local` message-tool sends no longer report an available channel as unavailable. (#85022) Thanks @Kaspre.
- Agents/heartbeat: honor group/channel `message_tool` visible-reply policy and model-specific Codex runtime config for scheduled heartbeat runs, so failed internal tool output stays private. Fixes #85310. (#85357) Thanks @neeravmakwana.
- Gateway/ACP: close child ACP sessions spawned via `sessions_spawn` when their parent session is reset or deleted, instead of leaving orphaned `claude-agent-acp` processes that accumulate and exhaust memory. Fixes #68916. (#85190) Thanks @openperf.
- Codex app-server: block native execution paths when OpenClaw exec resolves to a node host while preserving the first-party CLI node binding path. Fixes #85012. (#85534) Thanks @joshavant.
- Diagnostics: bound cleanup timeout detail logs, emit drop summaries when async diagnostic bursts exceed the queue cap, and surface async queue drops through diagnostic telemetry.
- Agents/subagents: surface blocked child-run completions as errors instead of successful subagent finishes. (#80886) Thanks @TurboTheTurtle.
- Context engines: fail closed with a descriptive error when the selected agent runtime cannot satisfy declared context-engine host requirements.
- Agents/Pi: treat accepted embedded `sessions_spawn` child-session handoffs as terminal progress so parent turns no longer report false non-deliverable failures. (#85054) Thanks @samzong.
- CLI/models: resolve `openclaw models set` aliases from the runtime config while keeping authored aliases ahead of runtime-only defaults. (#83262) Thanks @IWhatsskill.
- Doctor: show personal Codex CLI asset notices as info instead of warnings. Fixes #84859.
- WhatsApp: update Baileys to `7.0.0-rc13` and drop the obsolete logger type patch.
- Install/update: reject OpenClaw GitHub source package targets early and point moving-main users at the dev/git install path instead of the broken npm source-install flow.
- CLI/update: pre-pack GitHub/git package update targets before the staged npm install, restoring `openclaw update --tag main` for one-off package updates. (#81296) Thanks @fuller-stack-dev.
- Gateway: mirror successful same-source message-tool sends into session transcripts so delivered replies stay in later history/context. (#84837) Thanks @iFiras-Max1.
- Media generation: keep image, music, and video completion delivery from duplicating or losing task ownership when generated media finishes through active session replies. (#84006) Thanks @fuller-stack-dev.
- CLI/doctor: remove stale bundled plugin load paths from old versioned OpenClaw package roots after pnpm/npm upgrades. Fixes #58626. Thanks @solink7.
- Infra/json: retry transient `File changed during read` races while loading JSON state so config and state reads recover instead of failing the turn. (#84285)
- Gateway/chat: surface message-tool-only room-event failures in chat diagnostics and session transcripts so suppressed source replies stay debuggable. Thanks @amknight.
- Plugins/providers: fail closed for workspace provider plugins during setup-mode discovery unless explicitly trusted, preventing untrusted workspace plugin code from running during provider setup. (#81069) Thanks @mmaps.
- Providers/Ollama: resolve configured Ollama Cloud `OLLAMA_API_KEY` markers to the real discovery key so cloud provider entries keep authenticated model catalog access. (#85037)
- Discord: keep persistent component registry fallback warnings actionable by forwarding structured error and cause metadata through the runtime logger. Fixes #84185. (#84190) Thanks @100menotu001.
@@ -46,8 +367,10 @@ Docs: https://docs.openclaw.ai
- Agents/config: keep non-Google provider model refs from being rewritten by Google Gemini preview-id normalization. (#84762) Thanks @zhangguiping-xydt.
- Installer: require a real controlling terminal before launching onboarding so headless `curl | bash` installs finish cleanly after installing the CLI.
- Agents/Codex: promote a completed final assistant response when a prompt timeout races Codex app-server completion instead of returning an empty timeout envelope. Refs #84516.
- Codex app-server: keep interrupted turn statuses from being treated as OpenClaw aborts by themselves, so tool-only turns remain eligible for no-visible-answer recovery. Fixes #84492.
- Agents: cap heartbeat model bleed context hints by the stored session window when runtime model metadata is unavailable, so overflow recovery advice does not suggest a larger window than the active session actually has.
- Control UI/Web Push: use `https://openclaw.ai` as the generated default VAPID subject instead of the old localhost mailbox so iOS PWA push setup uses an Apple-acceptable subject when `OPENCLAW_VAPID_SUBJECT` is unset. Fixes #83134. (#83317) Thanks @IWhatsskill.
- Control UI: distinguish inherited thinking-off settings from explicit Off selections so the thinking selector no longer shows two identical Off rows. (#85223) Thanks @amknight.
- Agents/Pi: keep embedded session transcript writes from tripping false takeover detection after packaged npm onboarding agent turns.
- Codex/TUI: surface Codex-native post-turn compaction failures instead of continuing uncompacted, and keep successful native compaction serialized before local idle/next-turn handling. Fixes #84305. (#85160) Thanks @joshavant.
- Memory/search: stop recall tracking from writing dreaming side-effect artifacts when `dreaming.enabled=false`, while preserving normal search results. Fixes #84436. (#84444) Thanks @NianJiuZst.
@@ -58,13 +381,16 @@ Docs: https://docs.openclaw.ai
- Update/doctor: prune stale local bundled plugin install records that point at old compiled bundled output so current bundled plugin schemas win after upgrade. (#84863) Thanks @fuller-stack-dev.
- Providers/Ollama: preserve native Ollama tool-call IDs across assistant replay so Gemini over Ollama Cloud can keep its hidden function-call thought-signature handle.
- Discord: keep session recovery and `/stop` abort ownership on the source dispatch lane while bound ACP turns continue routing to their target session, so stalled pre-run work and late replies are cleared instead of leaking after stop. Fixes #84477. (#85100) Thanks @joshavant.
- Discord/voice-call: keep forced realtime voice consult diagnostics in debug logs instead of agent prompts, so callers do not hear OpenClaw policy text when the provider misses `openclaw_agent_consult`. (#84411) Thanks @fuller-stack-dev.
- Codex app-server: mark missing turn completion after observed execution as replay-unsafe and release the session so follow-up turns can run. Fixes #84076. (#85107) Thanks @joshavant.
- Codex app-server: give visible `message` dynamic tool sends a longer timeout budget so slow channel delivery can return its own result or error instead of hitting the 30-second Codex wrapper. (#85216) Thanks @amknight.
- Codex app-server: add a dedicated post-tool raw assistant completion idle timeout config so trusted heavy turns can wait longer after tool handoff without weakening final assistant release.
- Matrix: keep explicitly configured two-person rooms on the room route before stale `m.direct` or strict two-member DM fallback can bypass mention gating. Fixes #85017. (#85137) Thanks @joshavant.
- Agents/subagents: require explicit subagent allowlist targets to be configured agents so stale deleted-agent ids are omitted from `agents_list` and rejected by `sessions_spawn`. Fixes #84811. (#85154) Thanks @joshavant.
- PDF tool: time out idle remote PDF body reads after 120 seconds so stalled remote documents return an error instead of wedging the session. Fixes #68649. (#84768) Thanks @luoyanglang.
- Diagnostics/OpenTelemetry plugin: suppress handled OTLP exporter promise rejections so collector shutdowns no longer crash the Gateway. (#81085) Thanks @luoyanglang.
- Agents/exec: omit raw command text and env values from denied exec failure logs while keeping safe correlation metadata. Fixes #85049. (#85140) Thanks @joshavant.
- Media-understanding: restore the 4096-token default for image descriptions so reasoning-capable vision models no longer truncate before returning text, while preserving smaller model caps. (#84932) Thanks @scotthuang.
- Media/audio: skip empty structured sherpa-onnx transcripts instead of treating the raw JSON payload as spoken text. (#84667) Thanks @TurboTheTurtle.
- Agents/exec: preserve inherited XDG base-directory environment values for subprocesses while still rejecting agent-supplied XDG overrides. Fixes #84854. (#85139) Thanks @joshavant.
- Node/Linux: keep `OPENCLAW_GATEWAY_TOKEN` out of generated systemd unit files by writing node service token values to a node-specific env file. (#84408)
@@ -72,20 +398,24 @@ Docs: https://docs.openclaw.ai
- Trajectory/support: tolerate partial skill snapshot entries when building support metadata so rejected skill path scans no longer abort trajectory capture. (#71185) Thanks @lukeboyett.
- TUI: coalesce repeated idle Esc abort notices into a single `no active run xN` system row instead of appending duplicate rows.
- Telegram: honor `channels.telegram.pollingStallThresholdMs` in the default isolated polling path, restarting silent workers instead of leaving inbound updates wedged. Fixes #83950. (#84861) Thanks @joshavant.
- Telegram: dedupe replayed message dispatches by Telegram chat/message identity so isolated-ingress replays do not trigger duplicate model dispatches. Fixes #84886. (#85208) Thanks @joshavant.
- Slack: suppress reasoning payloads before reply delivery and dispatch accounting, so Slack monitor, slash-command, fallback, and direct reply paths do not leak model reasoning. Fixes #84319. (#84322) Thanks @ffluk3 and @joshavant.
- Slack: deliver native plugin approval prompts and updates when Slack native approvals are enabled, while keeping plugin approval authorization separate from exec approvers.
- Slack: keep native plugin approval prompts in the originating app conversation thread when the live Slack turn source is a `D...` conversation.
- Agents/Pi: disable the embedded pi-coding-agent runtime auto-retry so OpenClaw's own retry and failover loop does not replay failed tool calls through a nested SDK retry. Fixes #73781. (#74434) Thanks @yelog.
- CLI/perf: keep `setup --help`, `onboard --help`, and `configure --help` out of the full wizard runtime while preserving the existing help output. (#84488) Thanks @frankekn.
- CLI/perf: keep `agents --help` out of agents action/runtime imports so help, completion, and command discovery paths avoid loading the full agents runtime. (#84483) Thanks @frankekn.
- CLI/perf: keep `secrets --help` and `nodes --help` on the precomputed help path so parent help avoids loading action-heavy command runtime modules. (#84818) Thanks @frankekn.
- CLI/perf: serve `doctor`, `gateway`, `models`, and `plugins` parent help from startup metadata so common subcommand help avoids full CLI program construction. (#84786) Thanks @frankekn.
- Codex/Lossless: keep context-engine history on the canonical run session when Telegram DMs use per-peer runtime policy keys. Fixes #84936. (#84954) Thanks @neeravmakwana.
- Codex: keep heartbeat response tool schemas durable without exposing dynamic tools disabled by turn policy, so heartbeat wakeups can reuse threads while scoped tool allowlists stay enforced. (#84681) Thanks @jalehman.
- Auth/OAuth: skip the refresh adapter when a stored OAuth credential has no refresh token so agent turns fail fast on missing-key instead of waiting on the 120s refresh timeout. Thanks @romneyda.
- Auth/Codex: load legacy OAuth sidecar credentials in the embedded runner's secrets-runtime auth loaders so Telegram replies, cron-triggered turns, and other isolated sub-agent lanes can reach the existing #83312 refresh-and-rewrite migration instead of failing with `No API key found for provider "openai-codex"` until the user runs `openclaw doctor`. Thanks @Totalsolutionsync and @romneyda.
- Codex/failover: classify `deactivated_workspace` as a permanent auth failure so configured fallback models can advance when a Codex workspace is deactivated. (#55893) Thanks @litang9.
- Exec: keep configured `tools.exec.pathPrepend` entries ahead of user shell startup PATH changes on POSIX gateway runs. (#81403) Thanks @medns.
- Gateway/sessions: allow shared-secret bearer callers to read and stream session history without an explicit scope header. (#81815) Thanks @medns.
- Agents/embedded runner: classify HTML auth provider responses as `auth_html` and return a re-authentication hint instead of the CDN-blocked copy that `upstream_html` returns. Cloudflare Access login pages, nginx basic-auth challenges, and gateway login walls all produce HTML auth bodies that were previously misdiagnosed as transient CDN blocks. (#79900) Thanks @martingarramon.
- TUI/streaming watchdog: dismiss the `This response is taking longer than expected` notice as soon as a chat event for the same run arrives, so the message no longer sits next to the recovered response when the run was only briefly silent. Refs #67052, #69081 (closed), prior attempt #69026. Thanks @jpruit20 and @romneyda.
- Agents/Pi: tolerate OpenClaw-owned transcript writes while embedded prompts are released for model I/O, keeping long-running Feishu, Slack, Telegram, and cron turns from failing with false session-takeover errors. Fixes #84059. (#84250) Thanks @tianxiaochannel-oss88.
## 2026.5.20
@@ -141,16 +471,19 @@ Docs: https://docs.openclaw.ai
- Discord: preserve disabled presentation buttons when adapting and rendering Discord message controls. (#84188) Thanks @100menotu001.
- Twitch: add a test-only client-manager registry reset helper so non-isolated Twitch tests can clear cached managers between cases. Fixes #83887. (#84244) Thanks @hclsys.
- Cron: run main-session scheduled work on a cron-owned wake lane while preserving reply delivery context, so background cron turns no longer block human main-session chat. Fixes #82766. (#82767) Thanks @galiniliev.
- Auto-reply/slash commands: require a word boundary after the matched prefix in `parseSlashCommandActionArgs` so `/config-check <args>` (or any skill that shares a built-in command prefix) is no longer captured by the shorter built-in handler. Fixes #84572. Thanks @infracore.
- Cron: use structured embedded-run denial metadata for isolated scheduled tasks so blocked exec requests fail the job without treating ordinary assistant prose as a denial. (#84067) Thanks @abnershang.
- Cron: keep recovered tool warnings diagnostic for successful scheduled runs so final cron output is delivered instead of being replaced by a post-processing warning. (#84045) Thanks @abnershang.
- Plugins/perf: thread explicit plugin discovery results through `loadBundledCapabilityRuntimeRegistry`, `resolveBundledPluginSources`, and `listChannelCatalogEntries` so callers that already hold a discovery result skip redundant filesystem walks. Thanks @SebTardif.
- harden update restart script creation [AI]. (#84088) Thanks @pgondhi987.
- Android/Control UI Talk: split realtime voice transcript turns, queue PCM playback writes, and add opt-in OpenClaw consult routing for Gateway relay when a realtime provider skips `openclaw_agent_consult`. (#84181) Thanks @VACInc.
- Docker: keep the bundled Codex plugin in official release image keep lists so the default OpenAI agent harness remains available after Docker pruning. Fixes #83613. (#83626) Thanks @YuanHanzhong.
- CLI/channels: preserve the first line of `openclaw channels logs` output when the rolling tail window starts exactly on a line boundary, mirroring the already-fixed `readLogSlice` behavior in `src/logging/log-tail.ts`.
- Control UI: treat terminal session status as authoritative over stale active-run flags so completed terminal runs stop showing abort/live UI. (#84057)
- CLI: preserve embedded equals signs in inline root option values instead of truncating after the second separator. (#83995) Thanks @ThiagoCAltoe.
- Matrix/config: accept `messages.queue.byChannel.matrix` queue overrides and keep queue provider schema/type keys aligned for Matrix, Google Chat, and Mattermost. Thanks @bdjben.
- CLI: format `openclaw acp client` failures through the shared error formatter so object-shaped errors stay readable instead of printing `[object Object]`. Fixes #83904. (#84080)
- Agents/message-tool: normalize non-canonical message body aliases (`SendMessage`, `content`, `text`) to `message` before send validation so model-emitted tool calls with aliased body keys are delivered instead of rejected. (#84079)
- Providers/Ollama: default unknown-capabilities models to tool-capable so discovered native Ollama models can use tools when `/api/show` omits capabilities. (#84055) Thanks @dutifulbob.
- Codex app-server: disable native Code Mode, user MCP, and app-backed plugin execution while OpenClaw sandboxing is active, routing shell access through `sandbox_exec`/`sandbox_process` instead. (#84388) Thanks @joshavant.
- Installer/Windows: launch `install.ps1` onboarding as an attached child process so fresh native Windows installs do not freeze visibly at `Starting setup...` or corrupt the wizard's terminal rendering.
@@ -167,6 +500,7 @@ Docs: https://docs.openclaw.ai
- Agents/Codex: keep encrypted Responses reasoning replay provenance-bound so stale mirrored Codex transcripts drop invalid encrypted content before request assembly while preserving matching same-session replay. Fixes #83836. (#84367) Thanks @joshavant.
- Agents/subagents: skip stale embedded-run wake probes for dormant completion requesters, so late subagent completions go straight to requester-agent/direct handoff instead of producing `reason=no_active_run` queue noise. (#82964) Thanks @galiniliev.
- CLI: retry config snapshot reads after a transient failure so one rejected read no longer poisons later commands in the same process. (#83931) Thanks @honor2030.
- TUI: handle German-layout Kitty keyboard input by ignoring printable release events and accepting AltGr-produced printable characters such as `@` and `€`. Fixes #48897.
- Media: decode URL path basenames before using them as remote media fallback filenames, so files like `My%20Report.pdf` are surfaced as `My Report.pdf`. Fixes #84050. (#84052) Thanks @jbetala7.
- WhatsApp: clarify inbound group diagnostics so observed but unregistered groups point to `channels.whatsapp.groups` without changing routing or sender authorization. (#83846) Thanks @neeravmakwana.
- WhatsApp: drain pending outbound deliveries on a 30s periodic timer in addition to the reconnect handler, so messages enqueued while the provider is already connected no longer wait for the next reconnect to send. (#79083) Thanks @Oviemudiaga.
@@ -230,10 +564,12 @@ Docs: https://docs.openclaw.ai
### Fixes
- Agents/exec approvals: return approved WebChat gateway exec output inline after native approval instead of leaving the model waiting for an async follow-up. (#82019) Thanks @Zac-W.
- CLI/node: reject invalid explicit `node run --port` values instead of silently falling back to the configured or default port. Fixes #83923. Thanks @davinci282828.
- CLI: reject explicit port numbers above 65535 before they reach Gateway or Node bind paths. Fixes #83900. (#84008) Thanks @hclsys.
- Codex app-server: preserve plugin tool auth profiles when Codex owns model transport so OpenClaw dynamic tools can resolve their provider credentials. (#83603) Thanks @rubencu.
- Memory/search: scan the JS-side fallback vector path (used when the sqlite-vec index is unavailable or has a mismatched dimension) in bounded rowid batches and yield to the event loop between batches so large chunk tables can no longer pin the Node.js main thread for multi-second windows. Also keeps the SQL prepared statement rooted in a local so node:sqlite cannot finalize it mid-scan under heap pressure. Fixes #81172. Thanks @dev23xyz-oss.
- Backup: dereference hardlinks during archive creation and reject unsafe hardlink targets during verification so archives that pass `backup verify` do not fail broad extraction on macOS tar. Fixes #54242. Thanks @jason-allen-oneal.
- Memory Wiki: preserve fs-safe diagnostics when bridge source page writes fail for non-symlink filesystem safety reasons, so directory collisions are reported with the underlying error code. (#83776) Thanks @TurboTheTurtle.
- Telegram: keep forum topics from blocking sibling topic traffic by routing inbound serialization, media/text buffers, and account API queues on topic-aware lanes. (#83829)
- Telegram: keep queued forum-topic follow-up messages from inheriting superseded source abort signals, so later same-topic user turns can still run and reply after an active turn is replaced. (#83827) Thanks @VACInc.
@@ -966,7 +1302,7 @@ Docs: https://docs.openclaw.ai
- CLI/plugins: route lazy plugin command-registration chatter to stderr only during JSON-output command registration, keeping plugin-backed `--json` stdout parseable without changing parse-only or pass-through `--json` behavior. Fixes #81535. (#81536) Thanks @ScientificProgrammer and @vincentkoc.
- Plugins: treat git plugin install refs as refs instead of checkout flags, so option-like selectors fail checkout instead of silently installing the default branch. Fixes #79898. (#79901) Thanks @afurm and @vincentkoc.
- Doctor/memory: stop warning that no memory plugin is active when an enabled alternate memory plugin explicitly owns the memory slot, while preserving the warning for missing or disabled slot entries. Fixes #78540. (#78557) Thanks @carladams1299-lab and @vincentkoc.
- Plugins: keep process-local plugin metadata snapshot memo freshness tied to the cached registry snapshot so policy-stale derived plugin metadata edits invalidate the memo instead of returning stale owners or command aliases. (#81064) Thanks @Kaspre.
- Plugins: keep derived plugin metadata snapshots uncached when the persisted registry is missing, disabled, or stale, so newly added plugins are discovered without restarting. (#81064) Thanks @Kaspre.
- Plugins: discover provider plugins from `setup.providers[].envVars` credentials during provider discovery while keeping the deprecated `providerAuthEnvVars` fallback. (#81542) Thanks @JARVIS-Glasses.
- Docs/Codex harness: clarify that per-agent `CODEX_HOME` isolates `~/.codex` while inherited `HOME` intentionally keeps `.agents` discovery and subprocess user-home state available.
- CLI/plugins: keep bare plugin and parent-command help on the lightweight path, avoiding plugin registry discovery before rendering help.
@@ -2095,6 +2431,7 @@ Docs: https://docs.openclaw.ai
- Browser/chrome-mcp: read Chrome DevTools MCP screenshot output from the extension-suffixed path, fixing ENOENT on screenshot capture. Fixes #77222. (#74685) Thanks @barbarhan.
- Agents/OpenAI: honor `compat.supportsTools: false` for OpenAI Completions models so chat-only compatible endpoints do not receive `tools`, `tool_choice`, or tool-history fallback payloads. Fixes #74664. Thanks @yelog.
- macOS/launchd: set generated Gateway LaunchAgent plists to `ProcessType=Interactive` so the gateway keeps timely execution during idle periods. Fixes #58061; refs #62294 and closed duplicate #66992. (#62308) Thanks @bryanpearson and @zssggle-rgb.
- 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.
@@ -2146,6 +2483,7 @@ Docs: https://docs.openclaw.ai
- Diffs plugin: accept `defaults.ttlSeconds` as a plugin-wide artifact lifetime default, so LAN-viewable diff links can keep their configured six-hour TTL without doctor quarantining the plugin entry. (#77456) Thanks @VACInc.
- Gate zalouser startup name matching [AI]. (#77411) Thanks @pgondhi987.
- Active Memory: send a bounded latest-message search query to the recall worker so channel/runtime metadata does not become the memory search string. Fixes #65309. Thanks @joeykrug, @westley3601, @pimenov, and @tasi333.
- Memory/QMD: report missing or invalid agent workspace directories as workspace probe failures in doctor/QMD availability checks instead of sending operators toward binary-install fixes. Fixes #63158. Thanks @sercada.
- fix(device-pair): require pairing scope for pair command [AI]. (#76377) Thanks @pgondhi987.
- Providers/OpenRouter: keep DeepSeek V4 `reasoning_effort` on OpenRouter-supported values, mapping stale `max` thinking overrides to `xhigh` so `openrouter/deepseek/deepseek-v4-pro` no longer fails with OpenRouter's invalid-effort 400. Fixes #77350. (#77423) Thanks @krllagent, @mushuiyu886, and @sallyom.
- fix(qqbot): keep private commands off framework surface [AI]. (#77212) Thanks @pgondhi987.
@@ -2689,6 +3027,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- CLI/message: skip eager model context warmup and preserve channel-declared gateway execution for Discord and Telegram message actions, avoiding Codex app-server/model discovery during simple send/read commands. Thanks @fuller-stack-dev.
- Agents/exec approvals: parse exec approval result metadata with balanced parentheses so nested-paren denial and finished payloads such as `Exec denied (gateway id=req-1, approval-timeout (allowlist-miss)): ...` are matched and routed to the denied followup branch instead of falling through to the generic followup path. (#72268) Thanks @amittell.
- Codex/app-server: resolve managed binaries from bundled `dist` chunks and from the `@openai/codex` package bin when installs do not provide a nearby `.bin/codex` shim, avoiding false missing-binary startup failures.
- Plugins/ClawHub: use the ClawHub artifact resolver response as the install decision before downloading, keeping legacy ZIP fallback and future ClawPack npm-pack installs on the same explicit resolver path. Thanks @vincentkoc.
- Plugins/ClawHub: keep bare plugin package specs on npm for the launch cutover and reserve ClawHub resolution for explicit `clawhub:` specs until ClawHub pack readiness is deployed. Thanks @vincentkoc.
@@ -3010,6 +3349,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- Skills/OpenAI Whisper: restore executable bits for bundled Whisper and video-frame shell helpers and add a release check for non-executable bundled skill shell scripts, so packaged installs no longer fail with permission-denied errors. Fixes #9303. Thanks @nikolasdehor.
- Agents/tools: skip unavailable media generation and PDF tool factories from the live reply path when Gateway metadata and the active auth store prove no configured provider can back them, while keeping explicit config and auth-backed providers on the normal factory path. Thanks @shakkernerd.
- Agents/runtime: reuse the Gateway metadata startup plan when ensuring reply runtime plugins are loaded, so live agent turns do not broad-load plugin runtimes after the Gateway already scoped startup activation. Thanks @shakkernerd.
- Agents/runtime: delegate scoped reply runtime registry reuse to the plugin loader cache-key compatibility checks, so config changes with the same startup plugin ids cannot keep stale runtime hooks or tools active. Thanks @shakkernerd.
@@ -3034,6 +3374,7 @@ Docs: https://docs.openclaw.ai
- CLI/plugins: refresh persisted plugin registry policy in place for `plugins enable` and `plugins disable`, so routine toggles no longer rebuild and hash every plugin source when the target is already indexed. Thanks @vincentkoc.
- Windows/install: run npm from a writable installer temp directory and pin the Bedrock runtime dependency below a Windows ARM Node 24 npm resolver failure, so global OpenClaw installs no longer fail before onboarding. Thanks @mariozechner.
- CLI/plugins: scope install and enable slot selection to the selected plugin manifest/runtime fallback, so plugin installs no longer load every plugin runtime or broad status snapshot just to update memory/context slots. Thanks @vincentkoc.
- Browser/snapshot: propagate the configured snapshot timeout through the agent tool, Chrome MCP, and Playwright snapshot paths so snapshot actions honor the requested deadline instead of hanging. Fixes #72934. Thanks @masatohoshino.
- Plugins/TTS: keep bundled speech-provider discovery available on cold package Gateway paths and add bundled plugin matrix runtime probes for health, readiness, RPC, TTS discovery, and post-ready runtime-deps watchdog coverage. Refs #75283. Thanks @vincentkoc.
- Google Meet/Twilio: show delegated voice call ID, DTMF, and intro-greeting state in `googlemeet doctor`, and avoid claiming DTMF was sent when no Meet PIN sequence was configured. Refs #72478. Thanks @DougButdorf.
- Plugins/tools: prefer built bundled plugin code during tool discovery and skip channel runtime hydration while preserving companion provider registrations, reducing per-run plugin-tool prep cost without dropping executable plugin tools. Fixes #75290. Thanks @thanos-openclaw.
@@ -4925,7 +5266,8 @@ Docs: https://docs.openclaw.ai
- Providers/SDK retry: cap long `Retry-After` sleeps in Stainless-based Anthropic/OpenAI model SDKs so 60s+ retry windows surface immediately for OpenClaw failover instead of blocking the run. (#68474) Thanks @jetd1.
- Agents/TTS: preserve spoken text in TTS tool results while defusing reply directives in transcript content, so future turns remember voice replies without treating spoken `MEDIA:` or voice tags as delivery metadata. (#68869) Thanks @zqchris.
- Providers/OpenAI: harden Voice Call realtime transcription against OpenAI Realtime session-update drift, forward language and prompt hints, and add live coverage for realtime STT.
- Agents/Pi embedded runs: suppress the "⚠️ Agent couldn't generate a response" warning when the assistant already delivered user-visible content through a messaging tool and the turn ended cleanly (`stopReason=stop`). Real failure modes (tool errors, provider `stopReason=error`, interrupted tool use) still surface the existing "verify before retrying" warning. Fixes #70396. (#70425) Thanks @neeravmakwana.
- Agents/Pi embedded runs: suppress the "⚠️ Agent couldn't generate a response" warning when the assistant already delivered user-visible content through a messaging tool and the turn ended cleanly (`stopReason=stop`). Real failure modes (tool errors, provider `stopReason=error`, interrupted tool use) still surface the existing "verify before retrying" warning. Fixes #70396. (#70425) Thanks @neeravmakwana.
- Auto-reply/WebChat: preserve the active session mapping when context-overflow recovery or auto-compaction fails, and return retry, `/compact`, and `/new` guidance instead of silently rotating to a fresh session. Fixes #70472. (#70479) Thanks @fuller-stack-dev.
- Gateway/Linux: wrap gateway-managed supervisor, PTY, MCP stdio, and browser child processes in a tiny `/bin/sh` shim that raises the child's own `oom_score_adj` on Linux, so under cgroup memory pressure the kernel prefers transient workers over the long-lived gateway. Opt out with `OPENCLAW_CHILD_OOM_SCORE_ADJ=0`. Fixes #70404. (#70419) Thanks @neeravmakwana.
- Providers/Moonshot: stop strict-sanitizing Kimi's native tool_call IDs (shaped like `functions.<name>:<index>`) on the OpenAI-compatible transport, so multi-turn agentic flows through Kimi K2.6 no longer break after 2-3 tool-calling rounds when the serving layer fails to match mangled IDs against the original tool definitions. Adds a `sanitizeToolCallIds` opt-out to the shared `openai-compatible` replay family helper and wires Moonshot to it. Fixes #62319. (#70030) Thanks @LeoDu0314.
- Dependencies/security: override transitive `uuid` to `14.0.0`, clearing the runtime advisory across dependencies.

View File

@@ -60,7 +60,7 @@ COPY package.json pnpm-lock.yaml pnpm-workspace.yaml .npmrc ./
COPY openclaw.mjs ./
COPY ui/package.json ./ui/package.json
COPY patches ./patches
COPY scripts/postinstall-bundled-plugins.mjs scripts/preinstall-package-manager-warning.mjs scripts/npm-runner.mjs scripts/windows-cmd-helpers.mjs ./scripts/
COPY scripts/postinstall-bundled-plugins.mjs scripts/preinstall-package-manager-warning.mjs scripts/npm-runner.mjs scripts/windows-cmd-helpers.mjs scripts/prepare-git-hooks.mjs ./scripts/
COPY scripts/lib/package-dist-imports.mjs ./scripts/lib/package-dist-imports.mjs
COPY --from=workspace-deps /out/packages/ ./packages/
@@ -72,8 +72,7 @@ RUN --mount=type=cache,id=openclaw-pnpm-store,target=/root/.local/share/pnpm/sto
NODE_OPTIONS=--max-old-space-size=2048 pnpm install --frozen-lockfile \
--config.supportedArchitectures.os=linux \
--config.supportedArchitectures.cpu="$(node -p 'process.arch')" \
--config.supportedArchitectures.libc=glibc && \
pnpm store add source-map@0.6.1
--config.supportedArchitectures.libc=glibc
# pnpm v10+ may append peer-resolution hashes to virtual-store folder names; do not hardcode `.pnpm/...`
# paths. Matrix's native downloader can hit transient release CDN errors while
@@ -122,7 +121,10 @@ RUN pnpm_config_verify_deps_before_run=false pnpm qa:lab:build
FROM build AS runtime-assets
ARG OPENCLAW_EXTENSIONS
ARG OPENCLAW_BUNDLED_PLUGIN_DIR
# BuildKit cache mounts are not part of cached layers; seed tarballs for the
# installed prod graph in the same step that runs offline prune.
RUN --mount=type=cache,id=openclaw-pnpm-store,target=/root/.local/share/pnpm/store,sharing=locked \
pnpm list --prod --depth Infinity --json | node scripts/list-prod-store-packages.mjs | xargs -r pnpm store add && \
CI=true pnpm prune --prod \
--config.offline=true \
--config.supportedArchitectures.os=linux \
@@ -283,10 +285,15 @@ RUN --mount=type=cache,id=openclaw-bookworm-apt-cache,target=/var/cache/apt,shar
RUN ln -sf /app/openclaw.mjs /usr/local/bin/openclaw \
&& chmod 755 /app/openclaw.mjs
# Pre-create the default state dir so first-run Docker named volumes mounted
# here inherit node ownership instead of root-owned state.
RUN install -d -m 0700 -o node -g node /home/node/.openclaw && \
stat -c '%U:%G %a' /home/node/.openclaw | grep -qx 'node:node 700'
# Pre-create default named-volume mount points so first-run Docker volumes copy
# node ownership from the image instead of starting as root-owned directories.
RUN install -d -m 0700 -o node -g node \
/home/node/.openclaw \
/home/node/.openclaw/workspace \
/home/node/.config/openclaw && \
stat -c '%U:%G %a' /home/node/.openclaw | grep -qx 'node:node 700' && \
stat -c '%U:%G %a' /home/node/.openclaw/workspace | grep -qx 'node:node 700' && \
stat -c '%U:%G %a' /home/node/.config/openclaw | grep -qx 'node:node 700'
ENV NODE_ENV=production

View File

@@ -92,7 +92,7 @@ Works with npm, pnpm, or bun.
- **[OpenAI](https://openai.com/)** (ChatGPT/Codex)
Model note: while many providers and models are supported, prefer a current flagship model from the provider you trust and already use. See [Onboarding](https://docs.openclaw.ai/start/onboarding).
Model note: while many providers and models are supported, prefer a current flagship model from the provider you trust and already use. See [Onboarding](https://docs.openclaw.ai/start/wizard).
## Install (recommended)
@@ -113,11 +113,23 @@ Runtime: **Node 24 (recommended) or Node 22.19+**.
Full beginner guide (auth, pairing, channels): [Getting started](https://docs.openclaw.ai/start/getting-started)
Recommended daemon mode:
```bash
openclaw onboard --install-daemon
openclaw gateway status
```
Foreground/debug mode:
```bash
openclaw gateway stop
openclaw gateway --port 18789 --verbose
```
Send a test message or ask the assistant after either startup mode is running:
```bash
# Send a message
openclaw message send --target +1234567890 --message "Hello from OpenClaw"
@@ -133,7 +145,8 @@ Models config + CLI: [Models](https://docs.openclaw.ai/concepts/models). Auth pr
OpenClaw connects to real messaging surfaces. Treat inbound DMs as **untrusted input**.
Full security guide: [Security](https://docs.openclaw.ai/gateway/security)
Full security guide: [Security](https://docs.openclaw.ai/gateway/security).
Before remote exposure, use the [Gateway exposure runbook](https://docs.openclaw.ai/gateway/security/exposure-runbook).
Default behavior on Telegram/WhatsApp/Signal/iMessage/Microsoft Teams/Discord/Google Chat/Slack:
@@ -159,7 +172,7 @@ Run `openclaw doctor` to surface risky/misconfigured DM policies.
- Default: tools run on the host for the `main` session, so the agent has full access when it is just you.
- Group/channel safety: set `agents.defaults.sandbox.mode: "non-main"` to run non-`main` sessions inside sandboxes. Docker is the default sandbox backend; SSH and OpenShell backends are also available.
- Typical sandbox default: allow `bash`, `process`, `read`, `write`, `edit`, `sessions_list`, `sessions_history`, `sessions_send`, `sessions_spawn`; deny `browser`, `canvas`, `nodes`, `cron`, `discord`, `gateway`.
- Before exposing anything remotely, read [Security](https://docs.openclaw.ai/gateway/security), [Sandboxing](https://docs.openclaw.ai/gateway/sandboxing), and [Configuration](https://docs.openclaw.ai/gateway/configuration).
- Before exposing anything remotely, read [Security](https://docs.openclaw.ai/gateway/security), [Gateway exposure runbook](https://docs.openclaw.ai/gateway/security/exposure-runbook), [Sandboxing](https://docs.openclaw.ai/gateway/sandboxing), and [Configuration](https://docs.openclaw.ai/gateway/configuration).
## Operator quick refs
@@ -173,7 +186,7 @@ Run `openclaw doctor` to surface risky/misconfigured DM policies.
- New here: [Getting started](https://docs.openclaw.ai/start/getting-started), [Onboarding](https://docs.openclaw.ai/start/wizard), [Updating](https://docs.openclaw.ai/install/updating)
- Channel setup: [Channels index](https://docs.openclaw.ai/channels), [WhatsApp](https://docs.openclaw.ai/channels/whatsapp), [Telegram](https://docs.openclaw.ai/channels/telegram), [Discord](https://docs.openclaw.ai/channels/discord), [Slack](https://docs.openclaw.ai/channels/slack)
- Apps + nodes: [macOS](https://docs.openclaw.ai/platforms/macos), [iOS](https://docs.openclaw.ai/platforms/ios), [Android](https://docs.openclaw.ai/platforms/android), [Nodes](https://docs.openclaw.ai/nodes)
- Config + security: [Configuration](https://docs.openclaw.ai/gateway/configuration), [Security](https://docs.openclaw.ai/gateway/security), [Sandboxing](https://docs.openclaw.ai/gateway/sandboxing)
- Config + security: [Configuration](https://docs.openclaw.ai/gateway/configuration), [Security](https://docs.openclaw.ai/gateway/security), [Exposure runbook](https://docs.openclaw.ai/gateway/security/exposure-runbook), [Sandboxing](https://docs.openclaw.ai/gateway/sandboxing)
- Remote + web: [Gateway](https://docs.openclaw.ai/gateway), [Remote access](https://docs.openclaw.ai/gateway/remote), [Tailscale](https://docs.openclaw.ai/gateway/tailscale), [Web surfaces](https://docs.openclaw.ai/web)
- Tools + automation: [Tools](https://docs.openclaw.ai/tools), [Skills](https://docs.openclaw.ai/tools/skills), [Cron jobs](https://docs.openclaw.ai/automation/cron-jobs), [Webhooks](https://docs.openclaw.ai/automation/webhook), [Gmail Pub/Sub](https://docs.openclaw.ai/automation/gmail-pubsub)
- Internals: [Architecture](https://docs.openclaw.ai/concepts/architecture), [Agent](https://docs.openclaw.ai/concepts/agent), [Session model](https://docs.openclaw.ai/concepts/session), [Gateway protocol](https://docs.openclaw.ai/reference/rpc)

View File

@@ -2,6 +2,285 @@
<rss xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" version="2.0">
<channel>
<title>OpenClaw</title>
<item>
<title>2026.5.22</title>
<pubDate>Sun, 24 May 2026 01:41:27 +0000</pubDate>
<link>https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml</link>
<sparkle:version>2026052290</sparkle:version>
<sparkle:shortVersionString>2026.5.22</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
<description><![CDATA[<h2>OpenClaw 2026.5.22</h2>
<h3>Changes</h3>
<ul>
<li>Gateway/perf: reuse process-stable channel catalog reads, avoid repeated bundled-channel boundary checks, and rotate gateway watch CPU profiles so benchmark runs do not accumulate unbounded artifacts.</li>
<li>Gateway/perf: reuse immutable plugin metadata snapshots across startup, config, model, channel, setup, and secret metadata readers so hot paths avoid repeated plugin file stats and manifest registry reloads.</li>
<li>Gateway/perf: lazy-load startup-idle plugin work, core gateway method handlers, and the embedded ACPX runtime so Gateway health and ready signals no longer wait on unused handler trees or ACPX probes.</li>
<li>Gateway/perf: cache plugin SDK public-surface alias maps and skip irrelevant macOS Linuxbrew PATH probes so Gateway startup avoids repeated filesystem walks and slow missing-directory stats.</li>
<li>Meeting Notes: add a source-only external meeting-notes plugin and SDK source-provider contract outside the core npm package, with auto-start capture config, manual transcript imports, read-only <code>openclaw meeting-notes</code> CLI access, and Discord voice as the first live source.</li>
<li>Docs/channels/config: add Signal <code>configPath</code>, Telegram wildcard topic defaults, local-time backup archive names, Termux home fallback, include-path validation, secret-scanner-safe placeholder guidance, Gemini CLI/Antigravity media guidance, and macOS VM auto-login guidance. Thanks @NorseGaud, @yudistiraashadi, @huangqian8, @VibhorGautam, @maweibin, @tianxingleo, @IgnacioPro, and @xzcxzcyy-claw.</li>
<li>Docs: clarify model-usage portability, Codex migration prerequisites, status bootstrap wording, thread-bound subagent limits, hook ownership, and config-preserving safety guidance. Thanks @aniruddhaadak80, @leno23, @TomDjerry, @matthewxmurphy, @vincentkoc, and @stablegenius49.</li>
<li>Docs: clarify README onboarding and Gateway startup paths, WhatsApp QR/408 recovery, cron output language prompts, skill advanced features, gateway upstream 403 troubleshooting, and plugin fallback override guidance. Thanks @deepujain, @Zacxxx, @Jah-yee, @neyric, @usimic, @Renu-Cybe, @BigUncle, and @SeashoreShi.</li>
<li>Docs: clarify context-pruning ratio bounds, local dashboard recovery, CLI env markers, remote onboarding token behavior, and Peekaboo Bridge permissions for subprocess agents. Thanks @ayesha-aziz123, @dishraters, @hougangdev, and @brandonlipman.</li>
<li>Docs: clarify browser CDP diagnostics, Plugin SDK allowlist imports, status-reaction timing defaults, queue steering behavior, limited-tool troubleshooting, cron HEARTBEAT handling, Telegram multi-agent groups, Bitwarden SecretRef setup, and EasyRunner deployments. Thanks @Quratulain-bilal, @mbelinky, @Mickey-, @vancece, @xenouzik, @posigit, @surlymochan, @janaka, and @choiking.</li>
<li>Crabbox/Testbox: run clean sparse-checkout Testbox syncs from a temporary full checkout and route remote changed gates through Corepack pnpm.</li>
<li>Docs: clarify IPv4-only Gateway BYOH binding, trusted-proxy scope clearing, Android pairing approval, macOS Accessibility grants, Zalo profile env vars, password-store SecretRef setup, and Chinese memory navigation. Thanks @itskai-dev, @gwh7078, @longstoryscott, @MoeJaberr, and @yuaiccc.</li>
<li>Docs: consolidate GLM under Z.AI, add the Upstash Box install guide and Gateway exposure runbook, clarify MEDIA directives, Copilot and Voyage setup, config path quoting, real behavior proof, and memory-file write guidance. Thanks @BobDu, @alitariksahin, @Jefsky, @musaabhasan, @OmerZeyveli, @leno23, @WuKongAI-CMU, @luoyanglang, and @majin1102.</li>
<li>Docs: clarify media provider credentials, Codex/OpenClaw code-mode boundaries, Slack and Telegram ack reactions, Feishu dynamic agents, secrets plaintext boundaries, memory guidance, and Chinese glossary terms. Thanks @nielskaspers, @cosmopolitan033, @drclaw-iq, @alexgduarte, @zccyman, @chengoak, and @cassthebandit.</li>
<li>Packaging: exclude documentation images and assets from the npm tarball, reducing published package size without affecting runtime docs search or CLI behavior. Thanks @SebTardif.</li>
<li>Media understanding: stop auto-probing Gemini CLI and use Antigravity CLI only as a lower-priority image/video fallback after configured provider APIs.</li>
<li>Agents/subagents: limit default sub-agent bootstrap context to <code>AGENTS.md</code> and <code>TOOLS.md</code>, keeping persona, identity, user, memory, heartbeat, and setup files out of delegated workers by default. (#85283) Thanks @100yenadmin.</li>
<li>Maintainer skills: exclude plugin SDK/API boundary work from <code>openclaw-landable-bug-sweep</code> so bugbash sweeps stay focused on small paper-cut fixes.</li>
<li>QA-Lab/diagnostics: extend the OpenTelemetry smoke harness to prove trace, metric, and log export, and add first-class Prometheus and observability smoke aliases.</li>
<li>Plugin SDK: add a generic channel-message poll sender so channel plugins can expose poll delivery without depending on channel-specific SDK facades.</li>
<li>Crabbox: keep the local wrapper's provider validation synced with the installed Crabbox binary while preserving supported aliases such as <code>docker</code> and <code>blacksmith</code>. (#85302) Thanks @hxy91819.</li>
<li>Maintainer skills: add <code>openclaw-landable-bug-sweep</code> for producing five small, reviewed, CI-green OpenClaw bugfix PRs from issue/PR sweeps.</li>
<li>Control UI/chat: add search and Load More pagination to the chat session picker, keeping initial session loads bounded while making older conversations reachable. (#85237) Thanks @amknight.</li>
<li>CLI/onboarding: start classic onboarding when bare <code>openclaw</code> runs before an authored config exists, while keeping configured installs on Crestodian. (#72343) Thanks @fuller-stack-dev.</li>
<li>Discord: allow configuring a bounded <code>agentComponents.ttlMs</code> callback registry lifetime for long-running component workflows, with per-account overrides and a 24-hour cap. (#84189) Thanks @100menotu001.</li>
<li>xAI/Grok: reuse xAI OAuth auth profiles for Grok <code>web_search</code>, thread active-agent auth through web search, add Grok model aliases, and let media providers declare default operation timeouts. (#85182) Thanks @fuller-stack-dev.</li>
<li>Plugin SDK: add row-level session workflow helpers and deprecate <code>loadSessionStore</code> so plugins can read and patch sessions without depending on the legacy whole-store shape. (#84693) Thanks @efpiva.</li>
<li>Gateway/plugins: reuse a compatible Gateway startup plugin registry during dispatch so safe plugin dispatches avoid redundant registry loading. (#84324) Thanks @ai-hpc.</li>
<li>Plugins/SDK: add a general <code>embeddingProviders</code> capability contract and registration API so embeddings can become a reusable provider surface outside memory-specific adapters.</li>
<li>Dependencies: refresh provider, plugin, UI, and tooling packages, update <code>protobufjs</code> to 8.4.0 to clear the current npm advisory, and carry the Claude ACP completion patch forward to <code>@agentclientprotocol/claude-agent-acp</code> 0.36.1.</li>
<li>Agents/tools: remove the old sender-owner tool gating path so configured tools stay visible for trusted sessions while command and channel-action auth still carry real sender identity.</li>
<li>QA-Lab: add curated mock JSONL replay fixtures and first-drift reporting for runtime-parity audits. (#80323, refs #80176) Thanks @100yenadmin.</li>
<li>QA-Lab: add a QA bus tool-trace visibility scenario for sanitized tool-call assertions.</li>
<li>QA-Lab: replace generic evidence framing in seeded scenario prompts with concrete observed QA behavior.</li>
<li>QA-Lab: list named scenario packs in the coverage report so personal-agent privacy coverage stays visible in audits.</li>
<li>QA-Lab: list live transport lane membership in the coverage report so real transport checks stay separate from seeded qa-channel scenarios.</li>
<li>Release/package: run package integrity checks before package acceptance lanes so public install/update validation fails before private QA assets can leak into the package.</li>
<li>QA-Lab: include the optional 100-turn runtime parity soak in release-soak artifacts so long-run Codex/Pi transcript drift stays visible outside the default gate. (#80395) Thanks @100yenadmin.</li>
<li>QA-Lab: add a live-only long-context progress watchdog scenario for Codex app-server timeout and stalled-run sentinels. (#80323) Thanks @100yenadmin.</li>
<li>QA-Lab: tag gateway restart recovery and streaming final-integrity scenarios as live-only runtime parity lanes. (#80323) Thanks @100yenadmin.</li>
<li>QA-Lab: add a personal-agent failure recovery scenario that checks honest partial status, retry boundaries, and local recovery artifacts. (#83872) Thanks @iFiras-Max1.</li>
<li>QA-Lab: include an opt-in <code>update.run</code> package self-upgrade sentinel for destructive latest-package recovery checks.</li>
<li>QA-Lab: add Codex plugin lifecycle and auth-profile fixture coverage for missing installs, pinned-version drift, first-turn install ordering, and doctor migration safety. (#80323, refs #80174) Thanks @100yenadmin.</li>
<li>Models/perf: pre-warm the provider auth-state map at gateway startup so <code>/models</code> and every model-listing call short-circuits the per-provider plugin / external-CLI discovery on the hot path. Per-call cost drops from ~20 s to ~5 ms (~4,100×); the one-time startup warm resets and re-warms after hot reloads. (#84816) Thanks @sjf.</li>
<li>Release/security: ship the root npm package and OpenClaw-owned npm plugins with generated shrinkwrap, support bundled plugin runtime dependencies for suitable plugin tarballs, and require review for lockfile/shrinkwrap changes so published installs use locked dependency graphs.</li>
<li>Tests/perf: isolate doctor core health check unit coverage from real skills/workspace discovery so <code>doctor-core-checks</code> no longer dominates unit perf while keeping one real skills-readiness smoke. (#84493) Thanks @frankekn.</li>
</ul>
<h3>Fixes</h3>
<ul>
<li>WebChat: summarize internal message-tool source replies so tool cards no longer duplicate the visible reply body. (#84773) Thanks @jason-allen-oneal.</li>
<li>Gateway: preserve deferred lifecycle-error cleanup across later non-terminal events so provider timeouts can persist failed session state instead of leaving sessions stuck running. (#85256, fixes #63819) Thanks @samzong.</li>
<li>Agents/subagents: report tool-only child progress during timeout summaries instead of showing no visible output.</li>
<li>Telegram/ACP: preserve explicit <code>:topic:</code> conversation suffixes when inbound ACP targets do not carry a separate thread id.</li>
<li>Browser/proxy: bypass the managed proxy for the exact local managed Chrome CDP readiness and DevTools WebSocket endpoints, so <code>openclaw browser start</code> works when the operator proxy blocks loopback egress. (#83255) Thanks @lightcap.</li>
<li>Ollama: bypass the managed proxy for configured local embedding origins while keeping SSRF guardrails on unconfigured targets. Thanks @Kaspre.</li>
<li>OpenAI/images: route Codex API-key image generation through the native OpenAI Images API instead of the Codex OAuth streaming backend, avoiding 401s from valid API keys.</li>
<li>Agents/OpenAI completions: omit empty tool payload fields for proxy-like OpenAI-compatible endpoints so strict vLLM-style servers accept tool-free turns. (#85835) Thanks @rendrag-git.</li>
<li>Checks/Windows: route full <code>pnpm check</code> stage commands through the managed child runner so Windows avoids Node shell-argv deprecation warnings there too.</li>
<li>Checks/Windows: run managed child commands through explicit <code>cmd.exe</code> wrapping instead of Node shell mode with argv, avoiding Node 24 subprocess deprecation warnings during changed checks.</li>
<li>Gateway: omit internal stream-error placeholder entries from agent prompt history so failed assistant turns are not replayed as model-authored text. (#85652) Thanks @anyech.</li>
<li>Sessions: enforce the session write-lock max-hold policy during lock acquisition so long-held locks can be reclaimed before the stale-lock window. (#85764) Thanks @njuboy11.</li>
<li>Models: prune retired Groq, GitHub Copilot, OpenAI, xAI, and old Claude catalog entries, with doctor migration to upgrade existing configs to current provider refs.</li>
<li>Doctor/update: recognize junction-backed source checkouts as git installs by comparing canonical paths before showing package-manager update guidance. Fixes #82215. Thanks @igormf.</li>
<li>Channels: honor <code>/verbose on</code> for tool/progress summaries across direct chats, groups, channels, and forum topics while preserving quiet default behavior. (#85488) Thanks @kurplunkin.</li>
<li>CLI/skills: show an all-ready note with next-step commands when skill setup has no missing dependencies to install. (#85032) Thanks @aniruddhaadak80.</li>
<li>Microsoft Foundry: route DeepSeek V4 Pro and Flash models through the Foundry Responses API while keeping older DeepSeek models on their existing path. (#85549) Thanks @roslinmahmud.</li>
<li>Status/usage: show configured cost estimates for AWS SDK models in full usage output while keeping token-only usage replies cost-free. (#85619) Thanks @ItsOtherMauridian.</li>
<li>Agents/OpenAI Responses: retry non-visible reasoning-only turns for OpenAI Responses API families instead of treating them as empty failed turns. (#85603) Thanks @SebTardif.</li>
<li>Directive tags: preserve message and content-part object identity when display stripping makes no directive-tag changes. (#85682) Thanks @willamhou.</li>
<li>Telegram: send local <code>path</code>/<code>filePath</code> and structured attachment media from <code>sendMessage</code> actions instead of dropping them or sending text-only messages. (#85219) Thanks @keshavbotagent.</li>
<li>Sessions/status: show the estimated context budget when fresh provider usage is unavailable and clear stale estimates across session resets and compaction boundaries. (#84830) Thanks @giodl73-repo.</li>
<li>Gateway/config: pin relative <code>OPENCLAW_STATE_DIR</code> overrides to an absolute path at startup so later working-directory changes cannot retarget gateway state. (#52264) Thanks @PerfectPan.</li>
<li>Release/package: run npm release, prepublish, and postpublish verification through Windows-safe npm command shims so native Windows checks can execute <code>npm.cmd</code> instead of treating it as a binary.</li>
<li>Agents/harness: pass CLI runtime aliases through harness selection so provider-owned CLI aliases no longer get rejected before reaching the right runtime. (#85631) Thanks @potterdigital.</li>
<li>Secrets: show the irreversible apply warning after interactive <code>secrets configure</code> confirmation so confirmed migrations still get the final safety prompt. (#85638) Thanks @alkor2000.</li>
<li>Agents/CLI output: ignore cumulative Claude <code>stream-json</code> result usage when assistant usage events are present, preventing inflated cache-read accounting. (#85625) Thanks @zhouhe-xydt.</li>
<li>CLI: keep <code>waitForever()</code> alive by leaving its keep-alive interval ref'd so the public helper no longer exits immediately with Node's unsettled-await code. (#85694) Thanks @m1qaweb.</li>
<li>Agents/bootstrap: guard bootstrap name checks against missing file names so malformed bootstrap entries warn and truncate instead of crashing. Fixes #85523. (#85615) Thanks @zhouhe-xydt.</li>
<li>CLI/tasks: reject partially numeric <code>openclaw tasks audit --limit</code> values so audit limits must be real positive integers instead of accepting strings like <code>5abc</code>. (#84901) Thanks @jbetala7.</li>
<li>Status/diagnostics: bound deep Docker audit probes so <code>openclaw status --deep</code> reports slow container checks instead of hanging behind unbounded inspection. (#85476) Thanks @giodl73-repo.</li>
<li>Providers/Anthropic: migrate 1M context handling to GA-capable Claude 4.x models by sizing eligible models at 1M without the retired <code>context-1m-2025-08-07</code> beta, ignoring that retired beta in older configs, and preserving OAuth-required Anthropic beta headers. (#45613) Thanks @haoyu-haoyu.</li>
<li>Cron/Telegram: parse forum-topic delivery targets through the Telegram plugin instead of cron core, including <code>:topic:</code> and <code>:topicId</code> forms for announce delivery. Thanks @etticat.</li>
<li>Twitch: keep stale message-handler cleanup callbacks from removing newer handler registrations for the same account, preserving inbound message delivery after reconnects. Fixes #83888. (#85425) Thanks @alkor2000.</li>
<li>Memory/LanceDB: expose public memory artifacts through the active memory provider bridge so memory-wiki imports durable memory files, daily notes, dream reports, and event logs without depending on memory-core internals. Fixes #83604. (#85060) Thanks @brokemac79.</li>
<li>Crabbox: keep AWS hydration compatible with local Actions replay by inlining the hydrate workflow's Node/pnpm setup instead of invoking repo-local composite actions.</li>
<li>Agents/subagents: simplify native sub-agent completion handoff so children report their latest visible assistant result to the requester without using <code>message</code>, while keeping parent-owned message-tool delivery policy intact. Fixes #85070. (#85089) Thanks @brokemac79.</li>
<li>Docker setup: stop printing the Gateway bearer token in setup logs and printed follow-up commands.</li>
<li>Agents: let embedded compaction fallback retries proceed when PI-compatible candidates do not need agent harness plugin preparation.</li>
<li>Agents/tools: honor configured custom provider API keys when deciding whether media, image-generation, video-generation, music-generation, and PDF tools are available. (#85570)</li>
<li>StepFun: stop advertising stale generic API key auth choices so onboarding only offers runtime-backed Standard and Step Plan choices.</li>
<li>Diagnostics: keep OpenTelemetry log bodies behind explicit content capture and scrub scoped agent-session keys from OpenTelemetry and Prometheus labels while preserving bounded queue-lane prefixes.</li>
<li>Windows installer: fail Git checkout installs when <code>pnpm install</code> or <code>pnpm build</code> fails instead of writing a wrapper to a missing CLI build.</li>
<li>Sessions: surface previous-transcript archive failures during <code>/new</code> rotation so disk rename errors are logged instead of silently hiding stranded transcript files. Fixes #81984. (#85586, from #82081) Thanks @0xghost42.</li>
<li>TUI/agents: mirror internal-ui message-tool replies into final chat output so message-tool-only agents remain visible in <code>openclaw tui</code>. Fixes #85538. Thanks @danpolasek.</li>
<li>Agents: keep parallel OpenAI-compatible tool-call deltas in separate argument buffers so interleaved tool calls no longer corrupt streamed arguments. (#82263) Thanks @luna-system.</li>
<li>Memory/doctor: report missing or unusable QMD workspace directories as workspace failures instead of generic binary failures. (#63167) Thanks @sercada.</li>
<li>Debug proxy: record CONNECT client-socket errors and destroy the paired upstream socket so abrupt client disconnects no longer leak tunnel resources. (#82444) Thanks @SebTardif.</li>
<li>Diffs: continue hydrating later diff cards when one card fails so a single broken card no longer blanks the whole diff viewer. (#84775) Thanks @cosmopolitan033.</li>
<li>Mac app: use the native settings sidebar window chrome so the sidebar toggle stays on the left and content no longer clips under oversized titlebar padding.</li>
<li>QA-Lab/Codex: bundle auth/plugin fixture imports for flow scenarios and let terminal async media tools end Codex app-server turns without timing out. (#80397, refs #80323) Thanks @100yenadmin.</li>
<li>Gateway/agents: preserve fresh session overrides and metadata when stale cached agent-session entries race with store updates, so subagent model/provider overrides and routing policy survive concurrent writes. (#19328) Thanks @CodeReclaimers.</li>
<li>Control UI/chat: keep chat session search inline with the session selector so the header no longer shows a duplicate standalone search row.</li>
<li>Control UI/chat: collapse focused-mode header chrome and suppress hidden-header scroll updates so focus mode no longer jumps while scrolling. Thanks @amknight.</li>
<li>Codex app-server: restart the native app-server and retry once when server-side compaction times out, so preflight compaction stalls recover instead of failing every dispatch. (#85500)</li>
<li>Restore Control UI gateway token pairing [AI]. (#85459) Thanks @pgondhi987.</li>
<li>OpenAI video: honor configured provider request private-network opt-in for local/custom video endpoints so explicitly trusted mock and self-hosted providers are not blocked. Thanks @shakkernerd.</li>
<li>OpenAI video: send uploaded video edit requests to the documented <code>/videos/edits</code> endpoint with a <code>video</code> file instead of posting MP4 references to <code>/videos</code>. Thanks @shakkernerd.</li>
<li>Agents/channels: preserve message-tool delivery evidence through gateway agent completion handoffs so successful generated media sends are not followed by false failure messages. Thanks @shakkernerd.</li>
<li>CLI/update: repair managed npm plugin <code>openclaw</code> peer links during post-core convergence and reject stale or wrong-target peer links before restart. (#83794) Thanks @fuller-stack-dev.</li>
<li>CLI/agents: default new omitted-account bindings to all accounts when the channel has multiple configured accounts, and clarify account-scope docs. (#49769) Thanks @Gcaufy.</li>
<li>Codex app-server: let authorized <code>/codex</code> control commands such as <code>/codex detach</code> escape plugin-owned conversation bindings while keeping unknown or unauthorized slash text routed to the bound plugin. Fixes #85157. (#85188) Thanks @TurboTheTurtle.</li>
<li>Auto-reply/models: keep <code>/models</code> browse replies fast by sharing the bounded read-only catalog path with Gateway model listing. (#84735) Thanks @safrano9999.</li>
<li>Codex app-server: disable native Code Mode when the effective exec host is <code>node</code> and keep OpenClaw <code>exec</code>/<code>process</code> available, so <code>/exec host=node</code> routes shell commands through the selected node instead of the gateway. Fixes #85012. (#85090) Thanks @sahilsatralkar.</li>
<li>Agents: bound embedded auto-compaction session write-lock watchdogs to the compaction timeout instead of the full run timeout, so stuck compaction cannot hold the live session lock for the whole run window. (#84949) Thanks @luoyanglang.</li>
<li>Gateway/agents: return phase-aware <code>agent.wait</code> timeout attribution and only cool auth profiles on provider-started timeouts. Refs #65504. Thanks @100yenadmin.</li>
<li>Gateway: defer provider auth-state prewarm until after startup readiness so early gateway tool/session requests are not blocked by provider auth discovery. (#85272) Thanks @dutifulbob.</li>
<li>Gateway/models: coalesce provider auth-state rewarms after auth-profile failures and log event-loop delay for warm/rewarm work, so provider auth bursts no longer stack full auth sweeps behind channel replies.</li>
<li>Gateway/models: stop cancelled provider auth-state prewarms from continuing full provider sweeps, so reload and auth-failure bursts no longer keep startup busy.</li>
<li>Agents/Codex: show the first plan update as a transient chat status notice without counting it as final assistant content.</li>
<li>CLI/update: walk the macOS process ancestry and honor the inherited Gateway runtime PID before package updates stop the managed Gateway service, so nested in-band updater children can refuse instead of killing the LaunchAgent-supervised Gateway that owns them. Fixes #85120.</li>
<li>Gateway/LaunchAgent: wait for launchd reload bootout to finish and fall back to kickstart when bootstrap races, so reload handoff does not leave the service deregistered. Fixes #84630. (#84641) Thanks @NianJiuZst.</li>
<li>Gateway/LaunchAgent: treat a concurrent launchd bootstrap as a successful restart when the service is already loaded, avoiding false macOS Gateway restart failures. Fixes #84721. (#84722) Thanks @googlerest.</li>
<li>Gateway/service: include the active <code>openclaw</code> command bin directory in managed service PATH generation and doctor audit expectations for npm-global macOS installs. Fixes #84201. (#84475) Thanks @jbetala7.</li>
<li>Control UI/chat: disable the thinking selector for known non-reasoning models instead of showing duplicate Off choices. Fixes #84069. Thanks @DrippingMellow.</li>
<li>Memory: expand <code>~</code> in configured extra memory paths before resolving them, so home-relative folders are not treated as workspace-relative. Fixes #58026. Thanks @stadman.</li>
<li>Skills: treat <code>openclaw.os: macos</code> as Darwin when checking skill requirements, so macOS-only skills no longer report as missing on macOS hosts. Fixes #61338. Thanks @Jessecq1995.</li>
<li>Control UI/logs: strip ANSI escape sequences from displayed Gateway log messages so color codes no longer appear as raw text. Fixes #64399. Thanks @guguangxin-eng.</li>
<li>Docker: pre-create the workspace and auth-profile config mount points with <code>node</code> ownership so first-run named volumes do not start root-owned. Fixes #85076. Thanks @Noerr.</li>
<li>Telegram: pass configured markdown table mode through outbound markdown chunking so chunked sends render tables consistently. Fixes #85085. Thanks @ShuaiHui.</li>
<li>CLI/update: preserve managed Gateway service environment during package cutovers so macOS LaunchAgent repair/restart reads the pre-update service state instead of caller shell state. (#83026)</li>
<li>Agents/providers: honor per-model <code>api</code> and <code>baseUrl</code> overrides in custom provider auth hooks and transport selection. Fixes #80487. (#80488) Thanks @huveewomg.</li>
<li>Gateway/restart: eager-load the lifecycle runtime before in-place upgrade signal handling so package replacement does not deadlock restart imports. (#84890) Thanks @myps6415.</li>
<li>CLI/update: start managed Gateway update handoff helpers from a stable existing directory and tolerate deleted cwd/package roots during macOS LaunchAgent handoff. Fixes #83808. (#83875) Thanks @jason-allen-oneal.</li>
<li>Skills: watch each shared skill directory once across agent workspaces instead of once per agent, preventing file-descriptor exhaustion (<code>EMFILE</code>) that disposed bundle-mcp processes and stalled sessions on multi-agent gateways. Fixes #84968. (#85130) Thanks @openperf.</li>
<li>Release/security: keep generated npm shrinkwrap package versions inside the pnpm lock graph so published package locks cannot bypass pnpm dependency age and override policy.</li>
<li>Cron: honor <code>cron.retry.retryOn: ["network"]</code> for common network error codes such as <code>EAI_AGAIN</code>, <code>EHOSTUNREACH</code>, and <code>ENETUNREACH</code>.</li>
<li>Gateway chat: broadcast returned agent-run error payloads after an agent starts so ACP/WebChat clients receive terminal idle-timeout errors. Fixes #84945.</li>
<li>Gateway chat display: preserve OpenAI-compatible <code>prompt_tokens</code>, <code>completion_tokens</code>, and <code>total_tokens</code> usage fields in sanitized chat history so llama.cpp sessions keep context counts. Fixes #77992. Thanks @MarTT79.</li>
<li>Dashboard/CLI: allow macOS browser launching through <code>open</code> even when SSH environment variables are present, while preserving Linux SSH no-display protection. Fixes #67088. Thanks @theglove44.</li>
<li>Codex app-server: keep native web search observations out of mirrored chat transcripts while preserving tool progress telemetry. Fixes #85109. Thanks @ugitmebaby.</li>
<li>OpenCode Go: strip unsupported Kimi reasoning replay fields before provider requests so repeated <code>kimi-k2.6</code> turns do not fail schema validation. Fixes #83812. Thanks @Sleeck.</li>
<li>Browser/CDP: add a WSL2 portproxy self-loop hint when Chrome DevTools endpoints accept connections but return an empty HTTP reply. Fixes #59209. Thanks @Owlock.</li>
<li>Agents/OpenAI: preserve structured provider error code, type, and redacted body metadata on boundary-aware transport failures.</li>
<li>Doctor/Codex: point native Codex asset warnings at the canonical <code>openclaw migrate plan codex</code> preview command. Fixes #84948. Thanks @markoa.</li>
<li>CLI/models: make <code>capability model auth logout --agent</code> remove auth profiles from the selected non-default agent store. Fixes #85092. Thanks @islandpreneur007.</li>
<li>Gateway/models: reuse prepared provider auth metadata during model-listing auth checks so repeated lookups avoid broad plugin discovery while preserving synthetic local auth.</li>
<li>CLI/status: suppress systemd user-service setup hints when <code>openclaw status --deep</code> can already reach a running Gateway RPC service. Fixes #85094. Thanks @islandpreneur007.</li>
<li>CLI/devices: recover local approval when a same-device repair request replaces the request ID being approved.</li>
<li>CLI/agents: retry transient normal-close Gateway handshakes before falling back to embedded <code>openclaw agent</code> execution.</li>
<li>CLI/update: keep managed Gateway service stop/restart status lines out of <code>openclaw update --json</code> stdout so package-update automation can parse the JSON payload.</li>
<li>Plugins: resolve OpenClaw plugin SDK subpaths for native external plugin runtimes without mutating package installs or broadening process-wide module resolution.</li>
<li>Agents/OpenAI: preserve Responses and Chat Completions <code>reasoning_tokens</code> usage metadata without double-counting it in aggregate output tokens. (#85319)</li>
<li>Control UI/chat: convert pasted <code>data:image/...;base64,...</code> clipboard text into an image attachment instead of dumping the payload into the composer. Fixes #62604. Thanks @cpwilhelmi.</li>
<li>Providers/Gemini: strip fractional seconds from web-search time range filters so Gemini accepts freshness-bound search requests. (#85071) Thanks @Noerr.</li>
<li>OpenAI Codex: preserve image input support for sparse <code>openai-codex/gpt-5.5</code> catalog rows. (#85095) Thanks @sercada.</li>
<li>CLI/models: add a piped or pasted API-key path for OpenAI Codex auth and warn when API keys are pasted into token-mode auth. (#85533) Thanks @joshavant.</li>
<li>Telegram: dead-letter missing-harness isolated ingress failures so a poisoned spooled update no longer blocks later same-lane messages. Fixes #85470. (#85605) Thanks @joshavant.</li>
<li>Plugins/discovery: strip <code>-plugin</code> package suffixes when deriving plugin id hints so package names line up with manifest ids. (#85170) Thanks @JulyanXu.</li>
<li>Tlon: stop advertising a non-existent agent tool contract in the plugin manifest.</li>
<li>Telegram: preserve fenced code block languages through Markdown rendering so Telegram receives <code>language-*</code> code classes. (#85209) Thanks @leno23.</li>
<li>Windows installer: run npm and Corepack command shims from a Windows-local directory so installs launched from WSL2 UNC paths do not fail before OpenClaw is installed.</li>
<li>Windows updates: roll back git-backed updates to the previous checkout when dependency install, build, UI build, or doctor repair fails.</li>
<li>Windows installer: persist user-local portable Git on PATH and activate the repo-pinned pnpm version for git-backed installs and updates.</li>
<li>Windows installer: bootstrap a user-local portable Node.js when native Windows has no Node and no winget, Chocolatey, or Scoop, so first-run installs can continue on raw hosts.</li>
<li>Windows installer: extract the downloaded portable Node.js directory with native <code>tar</code> before falling back to .NET zip extraction, avoiding PowerShell 5.1 archive and path-length failures.</li>
<li>fix(integrations): enforce channel read target allowlists [AI]. (#84982) Thanks @pgondhi987.</li>
<li>Agents/heartbeat: route single-owner <code>session.dmScope=main</code> direct-message exec and cron event wakes back to the agent main session so async completions no longer strand context in orphan direct-DM queues. Fixes #71581. (#83743) Thanks @Kaspre.</li>
<li>Agents/code-mode: expose outer code-mode <code>exec</code> source through the <code>command</code> hook alias with <code>toolKind</code>/<code>toolInputKind</code> discriminators so exec-shaped policies can distinguish code-mode cells. (#83483) Thanks @Kaspre.</li>
<li>Agents/code mode: return structured timeout and runtime-unavailable error codes for known worker failures. Fixes #83389. (#83444) Thanks @Kaspre.</li>
<li>QA-Lab: isolate multi-scenario suite workers when scenarios need startup config patches, preventing message-routing config from leaking into unrelated scenarios.</li>
<li>QA-Lab: make the commitments heartbeat-target-none scenario request an immediate heartbeat instead of waiting for the next scheduled heartbeat.</li>
<li>Codex/Plugin SDK: deliver Codex-native subagent completions through a generic harness task runtime so harness-backed plugins can mirror durable task lifecycle and completion delivery without Codex-specific SDK imports. (#83445) Thanks @bryanpearson.</li>
<li>Gateway CLI: surface local post-challenge connect assembly failures immediately instead of waiting for the wrapper timeout. Fixes #68944. (#85253) Thanks @samzong.</li>
<li>Messages: strip unsupported web-search citation control markers from outbound replies before they reach WebChat or external channels. Fixes #85193. (#85204) Thanks @neeravmakwana.</li>
<li>Agents/exec: treat denied exec approvals as terminal instead of feeding them back into agent follow-up work, and recognize Chinese stop phrases in abort handling. Fixes #69386. (#85194) Thanks @samzong.</li>
<li>CLI/agents: abort accepted Gateway-backed <code>openclaw agent</code> runs on SIGINT/SIGTERM so cron and supervisor timeouts do not leave remote agent work alive. Fixes #71710. (#84381) Thanks @Kaspre.</li>
<li>Codex app-server: retry replay-safe stdio client-close turns once using structured failure metadata, while surfacing idle <code>turn/completed</code> timeouts instead of blindly replaying active shared-server turns. Thanks @VACInc.</li>
<li>Codex app-server: reject command overrides that embed Node or package-manager arguments and point users to <code>appServer.args</code>, so Windows startup avoids shell parsing failures. (#84417) Thanks @TurboTheTurtle.</li>
<li>Agents/Copilot: drop unsafe GitHub Copilot Responses reasoning replay items before send so Telegram direct sessions no longer fail on overlong replay IDs. Fixes #85197. (#85198) Thanks @galiniliev.</li>
<li>UI: add accessible tooltips to the topbar color-mode buttons so System, Light, and Dark choices are labeled on hover and focus. (#85227) Thanks @amknight.</li>
<li>fix: constrain Windows task script names [AI]. (#85064) Thanks @pgondhi987.</li>
<li>Control UI: keep the chat session picker from hiding older or cross-agent configured conversations while preserving the bounded configured-agent refresh. (#85211) Thanks @amknight.</li>
<li>Agents/Anthropic: preserve unsafe integer tool-call input values in streamed Anthropic tool-use JSON, preventing Discord-style IDs from being rounded before dispatch. Fixes #47229. (#83063) Thanks @leno23.</li>
<li>Agents/Codex: estimate tool-heavy prompt pressure at the LLM boundary before provider submission, so persistent sessions compact before overflowing context windows. (#85541) Thanks @fuller-stack-dev and @joshavant.</li>
<li>Agents/hooks: wait for local one-shot CLI and Codex <code>agent_end</code> plugin hooks before process cleanup so terminal observability flushes reliably. (#85007)</li>
<li>Providers/Google: preserve Gemini 3 cron <code>thinkingDefault: "low"</code> when stale catalog metadata says <code>reasoning:false</code>, so scheduled runs keep provider-supported thinking instead of downgrading to off. (#85185) Thanks @neeravmakwana.</li>
<li>CLI/agents: allow <code>openclaw agent --session-key</code> to target explicit session keys, including agent-scoped legacy keys. (#85121) Thanks @Kaspre.</li>
<li>Auto-reply/ACP: wait for same-channel block reply delivery before starting tool work, while still honoring ACP dispatch aborts so stopped turns do not wait on slow channel sends. (#83722) Thanks @IWhatsskill.</li>
<li>Codex/ACP: mark required child-run completions that only report progress, omit a final deliverable, or fail requester delivery as blocked while preserving real final reports. (#85110) Thanks @IWhatsskill.</li>
<li>Channels: treat bare abort messages such as <code>stop</code>, <code>abort</code>, and <code>wait</code> as immediate control commands in inbound debounce paths so stop requests are not delayed behind pending message coalescing. (#83348) Thanks @IWhatsskill.</li>
<li>Channels/message tool: resolve configured external channel plugins during in-agent channel selection, so <code>openclaw agent --local</code> message-tool sends no longer report an available channel as unavailable. (#85022) Thanks @Kaspre.</li>
<li>Agents/heartbeat: honor group/channel <code>message_tool</code> visible-reply policy and model-specific Codex runtime config for scheduled heartbeat runs, so failed internal tool output stays private. Fixes #85310. (#85357) Thanks @neeravmakwana.</li>
<li>Gateway/ACP: close child ACP sessions spawned via <code>sessions_spawn</code> when their parent session is reset or deleted, instead of leaving orphaned <code>claude-agent-acp</code> processes that accumulate and exhaust memory. Fixes #68916. (#85190) Thanks @openperf.</li>
<li>Codex app-server: block native execution paths when OpenClaw exec resolves to a node host while preserving the first-party CLI node binding path. Fixes #85012. (#85534) Thanks @joshavant.</li>
<li>Diagnostics: bound cleanup timeout detail logs, emit drop summaries when async diagnostic bursts exceed the queue cap, and surface async queue drops through diagnostic telemetry.</li>
<li>Agents/subagents: surface blocked child-run completions as errors instead of successful subagent finishes. (#80886) Thanks @TurboTheTurtle.</li>
<li>Context engines: fail closed with a descriptive error when the selected agent runtime cannot satisfy declared context-engine host requirements.</li>
<li>Agents/Pi: treat accepted embedded <code>sessions_spawn</code> child-session handoffs as terminal progress so parent turns no longer report false non-deliverable failures. (#85054) Thanks @samzong.</li>
<li>CLI/models: resolve <code>openclaw models set</code> aliases from the runtime config while keeping authored aliases ahead of runtime-only defaults. (#83262) Thanks @IWhatsskill.</li>
<li>Doctor: show personal Codex CLI asset notices as info instead of warnings. Fixes #84859.</li>
<li>WhatsApp: update Baileys to <code>7.0.0-rc13</code> and drop the obsolete logger type patch.</li>
<li>CLI/update: pre-pack GitHub/git package update targets before the staged npm install, restoring <code>openclaw update --tag main</code> for one-off package updates. (#81296) Thanks @fuller-stack-dev.</li>
<li>Gateway: mirror successful same-source message-tool sends into session transcripts so delivered replies stay in later history/context. (#84837) Thanks @iFiras-Max1.</li>
<li>Media generation: keep image, music, and video completion delivery from duplicating or losing task ownership when generated media finishes through active session replies. (#84006) Thanks @fuller-stack-dev.</li>
<li>Infra/json: retry transient <code>File changed during read</code> races while loading JSON state so config and state reads recover instead of failing the turn. (#84285)</li>
<li>Plugins/providers: fail closed for workspace provider plugins during setup-mode discovery unless explicitly trusted, preventing untrusted workspace plugin code from running during provider setup. (#81069) Thanks @mmaps.</li>
<li>Providers/Ollama: resolve configured Ollama Cloud <code>OLLAMA_API_KEY</code> markers to the real discovery key so cloud provider entries keep authenticated model catalog access. (#85037)</li>
<li>Discord: keep persistent component registry fallback warnings actionable by forwarding structured error and cause metadata through the runtime logger. Fixes #84185. (#84190) Thanks @100menotu001.</li>
<li>Gateway/sessions: preserve compatible session auth profile overrides when switching models within the same provider, including provider-auth aliases. Fixes #81837. (#81886) Thanks @TurboTheTurtle.</li>
<li>Gateway/status: surface inbound delivery telemetry counters and transport-liveness warnings in <code>openclaw status --all</code>. Fixes #49577. (#72724)</li>
<li>Docker: prune package-excluded plugin source workspaces and dependency closures so runtime images do not keep packages for plugins that were not opted in.</li>
<li>Providers/Ollama: treat Docker/OrbStack host aliases as local Ollama endpoints so <code>ollama-local</code> marker auth works when OpenClaw runs inside a VM/container and Ollama runs on the host. Fixes #84875.</li>
<li>QA-Lab: keep explicitly searchable/deferred OpenClaw dynamic tool rows report-only by default so tool-coverage gates do not treat mock discovery gaps as hard product failures. (#80319) Thanks @100yenadmin.</li>
<li>Agents/config: keep non-Google provider model refs from being rewritten by Google Gemini preview-id normalization. (#84762) Thanks @zhangguiping-xydt.</li>
<li>Installer: require a real controlling terminal before launching onboarding so headless <code>curl | bash</code> installs finish cleanly after installing the CLI.</li>
<li>Agents/Codex: promote a completed final assistant response when a prompt timeout races Codex app-server completion instead of returning an empty timeout envelope. Refs #84516.</li>
<li>Codex app-server: keep interrupted turn statuses from being treated as OpenClaw aborts by themselves, so tool-only turns remain eligible for no-visible-answer recovery. Fixes #84492.</li>
<li>Agents: cap heartbeat model bleed context hints by the stored session window when runtime model metadata is unavailable, so overflow recovery advice does not suggest a larger window than the active session actually has.</li>
<li>Control UI/Web Push: use <code>https://openclaw.ai</code> as the generated default VAPID subject instead of the old localhost mailbox so iOS PWA push setup uses an Apple-acceptable subject when <code>OPENCLAW_VAPID_SUBJECT</code> is unset. Fixes #83134. (#83317) Thanks @IWhatsskill.</li>
<li>Control UI: distinguish inherited thinking-off settings from explicit Off selections so the thinking selector no longer shows two identical Off rows. (#85223) Thanks @amknight.</li>
<li>Agents/Pi: keep embedded session transcript writes from tripping false takeover detection after packaged npm onboarding agent turns.</li>
<li>Codex/TUI: surface Codex-native post-turn compaction failures instead of continuing uncompacted, and keep successful native compaction serialized before local idle/next-turn handling. Fixes #84305. (#85160) Thanks @joshavant.</li>
<li>Memory/search: stop recall tracking from writing dreaming side-effect artifacts when <code>dreaming.enabled=false</code>, while preserving normal search results. Fixes #84436. (#84444) Thanks @NianJiuZst.</li>
<li>Diffs: render viewer toolbar icons from a closed icon-name map instead of HTML strings, removing the toolbar icon XSS sink. (#83955) Thanks @tanshanshan.</li>
<li>QA: keep <code>pnpm qa:e2e</code> self-check runs inside the private QA runtime envelope even when inherited shell env disables bundled plugins.</li>
<li>fix(config): validate browser sandbox bind sources [AI]. (#84799) Thanks @pgondhi987.</li>
<li>doctor: constrain legacy plugin cleanup paths [AI]. (#84801) Thanks @pgondhi987.</li>
<li>Update/doctor: prune stale local bundled plugin install records that point at old compiled bundled output so current bundled plugin schemas win after upgrade. (#84863) Thanks @fuller-stack-dev.</li>
<li>Providers/Ollama: preserve native Ollama tool-call IDs across assistant replay so Gemini over Ollama Cloud can keep its hidden function-call thought-signature handle.</li>
<li>Discord: keep session recovery and <code>/stop</code> abort ownership on the source dispatch lane while bound ACP turns continue routing to their target session, so stalled pre-run work and late replies are cleared instead of leaking after stop. Fixes #84477. (#85100) Thanks @joshavant.</li>
<li>Codex app-server: mark missing turn completion after observed execution as replay-unsafe and release the session so follow-up turns can run. Fixes #84076. (#85107) Thanks @joshavant.</li>
<li>Codex app-server: give visible <code>message</code> dynamic tool sends a longer timeout budget so slow channel delivery can return its own result or error instead of hitting the 30-second Codex wrapper. (#85216) Thanks @amknight.</li>
<li>Codex app-server: add a dedicated post-tool raw assistant completion idle timeout config so trusted heavy turns can wait longer after tool handoff without weakening final assistant release.</li>
<li>Matrix: keep explicitly configured two-person rooms on the room route before stale <code>m.direct</code> or strict two-member DM fallback can bypass mention gating. Fixes #85017. (#85137) Thanks @joshavant.</li>
<li>Agents/subagents: require explicit subagent allowlist targets to be configured agents so stale deleted-agent ids are omitted from <code>agents_list</code> and rejected by <code>sessions_spawn</code>. Fixes #84811. (#85154) Thanks @joshavant.</li>
<li>PDF tool: time out idle remote PDF body reads after 120 seconds so stalled remote documents return an error instead of wedging the session. Fixes #68649. (#84768) Thanks @luoyanglang.</li>
<li>Diagnostics/OpenTelemetry plugin: suppress handled OTLP exporter promise rejections so collector shutdowns no longer crash the Gateway. (#81085) Thanks @luoyanglang.</li>
<li>Agents/exec: omit raw command text and env values from denied exec failure logs while keeping safe correlation metadata. Fixes #85049. (#85140) Thanks @joshavant.</li>
<li>Media/audio: skip empty structured sherpa-onnx transcripts instead of treating the raw JSON payload as spoken text. (#84667) Thanks @TurboTheTurtle.</li>
<li>Agents/exec: preserve inherited XDG base-directory environment values for subprocesses while still rejecting agent-supplied XDG overrides. Fixes #84854. (#85139) Thanks @joshavant.</li>
<li>Node/Linux: keep <code>OPENCLAW_GATEWAY_TOKEN</code> out of generated systemd unit files by writing node service token values to a node-specific env file. (#84408)</li>
<li>Memory-core/dreaming: reuse stable narrative subagent session keys per workspace and phase while keeping per-run idempotency and bounded cleanup, so stale <code>dreaming-narrative-*</code> sessions do not accumulate. Fixes #68252, #69187, and #70402. (#70464) Thanks @chiyouYCH.</li>
<li>Trajectory/support: tolerate partial skill snapshot entries when building support metadata so rejected skill path scans no longer abort trajectory capture. (#71185) Thanks @lukeboyett.</li>
<li>TUI: coalesce repeated idle Esc abort notices into a single <code>no active run xN</code> system row instead of appending duplicate rows.</li>
<li>Telegram: honor <code>channels.telegram.pollingStallThresholdMs</code> in the default isolated polling path, restarting silent workers instead of leaving inbound updates wedged. Fixes #83950. (#84861) Thanks @joshavant.</li>
<li>Telegram: dedupe replayed message dispatches by Telegram chat/message identity so isolated-ingress replays do not trigger duplicate model dispatches. Fixes #84886. (#85208) Thanks @joshavant.</li>
<li>Slack: suppress reasoning payloads before reply delivery and dispatch accounting, so Slack monitor, slash-command, fallback, and direct reply paths do not leak model reasoning. Fixes #84319. (#84322) Thanks @ffluk3 and @joshavant.</li>
<li>Slack: deliver native plugin approval prompts and updates when Slack native approvals are enabled, while keeping plugin approval authorization separate from exec approvers.</li>
<li>Slack: keep native plugin approval prompts in the originating app conversation thread when the live Slack turn source is a <code>D...</code> conversation.</li>
<li>Agents/Pi: disable the embedded pi-coding-agent runtime auto-retry so OpenClaw's own retry and failover loop does not replay failed tool calls through a nested SDK retry. Fixes #73781. (#74434) Thanks @yelog.</li>
<li>CLI/perf: keep <code>setup --help</code>, <code>onboard --help</code>, and <code>configure --help</code> out of the full wizard runtime while preserving the existing help output. (#84488) Thanks @frankekn.</li>
<li>CLI/perf: keep <code>agents --help</code> out of agents action/runtime imports so help, completion, and command discovery paths avoid loading the full agents runtime. (#84483) Thanks @frankekn.</li>
<li>CLI/perf: keep <code>secrets --help</code> and <code>nodes --help</code> on the precomputed help path so parent help avoids loading action-heavy command runtime modules. (#84818) Thanks @frankekn.</li>
<li>CLI/perf: serve <code>doctor</code>, <code>gateway</code>, <code>models</code>, and <code>plugins</code> parent help from startup metadata so common subcommand help avoids full CLI program construction. (#84786) Thanks @frankekn.</li>
<li>Codex/Lossless: keep context-engine history on the canonical run session when Telegram DMs use per-peer runtime policy keys. Fixes #84936. (#84954) Thanks @neeravmakwana.</li>
<li>Codex: keep heartbeat response tool schemas durable without exposing dynamic tools disabled by turn policy, so heartbeat wakeups can reuse threads while scoped tool allowlists stay enforced. (#84681) Thanks @jalehman.</li>
<li>Auth/OAuth: skip the refresh adapter when a stored OAuth credential has no refresh token so agent turns fail fast on missing-key instead of waiting on the 120s refresh timeout. Thanks @romneyda.</li>
<li>Auth/Codex: load legacy OAuth sidecar credentials in the embedded runner's secrets-runtime auth loaders so Telegram replies, cron-triggered turns, and other isolated sub-agent lanes can reach the existing #83312 refresh-and-rewrite migration instead of failing with <code>No API key found for provider "openai-codex"</code> until the user runs <code>openclaw doctor</code>. Thanks @Totalsolutionsync and @romneyda.</li>
<li>Codex/failover: classify <code>deactivated_workspace</code> as a permanent auth failure so configured fallback models can advance when a Codex workspace is deactivated. (#55893) Thanks @litang9.</li>
<li>Exec: keep configured <code>tools.exec.pathPrepend</code> entries ahead of user shell startup PATH changes on POSIX gateway runs. (#81403) Thanks @medns.</li>
<li>Gateway/sessions: allow shared-secret bearer callers to read and stream session history without an explicit scope header. (#81815) Thanks @medns.</li>
<li>Agents/embedded runner: classify HTML auth provider responses as <code>auth_html</code> and return a re-authentication hint instead of the CDN-blocked copy that <code>upstream_html</code> returns. Cloudflare Access login pages, nginx basic-auth challenges, and gateway login walls all produce HTML auth bodies that were previously misdiagnosed as transient CDN blocks. (#79900) Thanks @martingarramon.</li>
<li>TUI/streaming watchdog: dismiss the <code>This response is taking longer than expected</code> notice as soon as a chat event for the same run arrives, so the message no longer sits next to the recovered response when the run was only briefly silent. Refs #67052, #69081 (closed), prior attempt #69026. Thanks @jpruit20 and @romneyda.</li>
<li>Agents/Pi: tolerate OpenClaw-owned transcript writes while embedded prompts are released for model I/O, keeping long-running Feishu, Slack, Telegram, and cron turns from failing with false session-takeover errors. Fixes #84059. (#84250) Thanks @tianxiaochannel-oss88.</li>
</ul>
<p><a href="https://github.com/openclaw/openclaw/blob/main/CHANGELOG.md">View full changelog</a></p>
]]></description>
<enclosure url="https://github.com/openclaw/openclaw/releases/download/v2026.5.22/OpenClaw-2026.5.22.zip" length="54409357" type="application/octet-stream" sparkle:edSignature="am1mwLOmUHor9QuQWtxSsKoBOCySUBo4fB+0Qdcrz0E3wf6ESIMTfOC0k+dKJSh9gtLZw5jzpWVqTBzEdU36Aw=="/>
</item>
<item>
<title>2026.5.20</title>
<pubDate>Thu, 21 May 2026 21:19:52 +0000</pubDate>
@@ -394,221 +673,5 @@
]]></description>
<enclosure url="https://github.com/openclaw/openclaw/releases/download/v2026.5.19/OpenClaw-2026.5.19.zip" length="54062201" type="application/octet-stream" sparkle:edSignature="7bVi6rv+TjhrUfi32V62BW2VgyV17jm7x+H6p10PRClCdXKZjhM7AX6MyvAz2+e7kzXIknj1Y9X7q43/E9fBBw=="/>
</item>
<item>
<title>2026.5.18</title>
<pubDate>Mon, 18 May 2026 22:41:13 +0000</pubDate>
<link>https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml</link>
<sparkle:version>2026051890</sparkle:version>
<sparkle:shortVersionString>2026.5.18</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
<description><![CDATA[<h2>OpenClaw 2026.5.18</h2>
<h3>Changes</h3>
<ul>
<li>Agents: clarify that fixes should default to clean bounded refactors, lean internals, and explicit plugin SDK/API deprecation paths.</li>
<li>Dependencies: update <code>@openclaw/proxyline</code> to 0.3.3.</li>
<li>Dependencies: update Pi packages to 0.75.1 and raise the minimum supported Node.js 22 line to 22.19.</li>
<li>Docker/Podman: add <code>OPENCLAW_IMAGE_APT_PACKAGES</code> as the runtime-neutral image build arg for extra apt packages while keeping <code>OPENCLAW_DOCKER_APT_PACKAGES</code> as a legacy fallback. (#62431) Thanks @urtabajev.</li>
<li>Gateway/ACPX: attribute startup probe, config, runtime, and resource-count costs in restart traces without changing readiness behavior. (#83300) Thanks @samzong.</li>
<li>Gateway: overlap startup logging and plugin-service startup with channel sidecars to reduce restart ready latency while preserving <code>/readyz</code> sidecar gating. (#83301) Thanks @samzong.</li>
<li>Plugins/admin-http-rpc: allow trusted admin HTTP RPC clients to start and wait for web QR login flows. (#83259) Thanks @liorb-mountapps.</li>
<li>Mac app: redesign Settings pages with consistent card layouts, cached navigation, cleaner permissions/voice/skills/cron/exec/debug panes, and steadier spacing around the native sidebar.</li>
<li>Skills: rename the repo-local Codex closeout review skill and helper to <code>autoreview</code> while preserving the Codex-first fallback behavior.</li>
<li>Skills: add a meme-maker skill for curated template search, local SVG/PNG rendering, Imgflip hosted rendering, and Know Your Meme provenance links.</li>
<li>Browser: surface pending and recently handled modal dialogs in snapshots, return <code>blockedByDialog</code> when an action opens a modal, and allow <code>browser dialog --dialog-id</code> to answer pending dialogs.</li>
<li>Agents/tools: shorten built-in tool descriptions and schema hints across media, messaging, sessions, cron, Gateway, web, image/PDF, TTS, nodes, and plan tools while preserving routing guardrails.</li>
<li>Skills: add node inspector debugging, fused diagram generation, and throwaway spike workflow skills.</li>
<li>CLI/plugins: add <code>defineToolPlugin</code> plus <code>openclaw plugins build</code>, <code>validate</code>, and <code>init</code> for typed simple tool plugins with generated manifest metadata, optional tool declarations, and context factories.</li>
<li>Agents/skills: tighten bundled skill prompts and metadata, quote skill descriptions, refresh current CLI/API guidance, and update embedded sherpa-onnx runtime downloads.</li>
<li>Skills: update the Obsidian skill to target the official <code>obsidian</code> CLI and require its registered binary instead of the third-party <code>obsidian-cli</code>.</li>
<li>Skills: add a Python debugging skill for pdb, breakpoint(), post-mortem inspection, and debugpy remote attach.</li>
<li>Plugins/messages: add presentation capability limits for channel renderers, adapt rich message controls before native rendering, and mark legacy <code>interactive</code>/Slack directive producer APIs as deprecated.</li>
<li>Proxy: support HTTPS managed forward-proxy endpoints and scoped <code>proxy.tls.caFile</code> CA trust for proxy endpoint TLS. (#79171) Thanks @jesse-merhi.</li>
<li>QA-Lab: add first-hour 20-turn and optional 100-turn runtime parity scenarios, with tier metadata for standard and soak QA gates. Fixes #80338; refs #80337. Thanks @100yenadmin.</li>
<li>QA-Lab: add <code>openclaw qa suite --runtime-parity-tier</code> and wire the standard Codex-vs-Pi tier into release checks separately from optional/live-only/soak lanes. Fixes #80337. Thanks @100yenadmin.</li>
<li>QA-Lab: add a live-only Codex Pi-shaped Read vocabulary canary so runtime parity catches native workspace-read prompt compatibility drift. (#80323) Thanks @100yenadmin.</li>
<li>QA-Lab: add live-only harness self-health scenarios for plugin hook crashes, manifest contract errors, and WebChat direct-reply self-message routing. (#80323) Thanks @100yenadmin.</li>
<li>QA-Lab: add runtime tool fixture scenarios and coverage reporting for Codex-native workspace tools, OpenClaw dynamic tools, and optional plugin-backed tools. Fixes #80173. Thanks @100yenadmin.</li>
<li>QA-Lab: expose runtime tool fixture coverage through <code>openclaw qa coverage --tools</code>, with optional suite-summary evaluation for parity gate artifacts. Thanks @100yenadmin.</li>
<li>QA-Lab: schedule a live-frontier Codex-vs-Pi runtime token-efficiency artifact lane in the all-lanes QA workflow. Fixes #80175. Thanks @100yenadmin.</li>
<li>QA-Lab: hard-gate required OpenClaw dynamic runtime-tool drift in the standard Codex-vs-Pi tier with a blocking release-check verifier and publish the tool coverage report artifact. Fixes #80339; refs #80319. Thanks @100yenadmin.</li>
<li>QA-Lab: add the personal-agent approval-denial scenario so the benchmark pack verifies denied local reads stop cleanly without tool progress or fixture leaks. (#83150) Thanks @iFiras-Max1.</li>
<li>QA-Lab: extend the personal-agent benchmark pack with a local task followthrough scenario for proof-backed pending, blocked, and done status reporting. Thanks @iFiras-Max1.</li>
<li>Gateway/performance: add <code>pnpm test:restart:gateway</code> benchmark tooling for repeated restart readiness, downtime, trace, and resource-slope evidence. (#83299) Thanks @samzong.</li>
<li>Android: switch Talk Mode to realtime Gateway relay voice sessions with streaming mic input, realtime audio playback, tool-result bridging, and on-screen transcripts. (#83130) Thanks @sliekens.</li>
</ul>
<h3>Fixes</h3>
<ul>
<li>Discord/OpenAI: keep realtime Discord voice sessions hearing follow-up turns with OpenAI realtime and prebuffer assistant playback to avoid choppy starts. (#80505) Thanks @Solvely-Colin.</li>
<li>Media: prevent image metadata probing from invoking external decoder delegates on unrecognized image bytes, and stop fallback chaining after real processing errors.</li>
<li>Media: install Sharp with the root package and fall back to sips, Windows native imaging, ImageMagick, GraphicsMagick, or ffmpeg for image resizing/conversion when Sharp is unavailable. Fixes #83401. Thanks @scotthuang.</li>
<li>Telegram: deliver generated media completions back into forum topics by preserving topic IDs across requester-agent handoff. (#83556) Thanks @fuller-stack-dev.</li>
<li>Gateway: defer update-check startup until after readiness so package update checks no longer block sidecar-ready startup, while preserving update broadcasts and shutdown cleanup. (#83520) Thanks @samzong.</li>
<li>Telegram: keep <code>/btw</code> and read-only status commands from aborting active runs, and avoid retaining raw update payloads in timed-out spool tombstones. Refs #83272.</li>
<li>Agents/video: hide <code>video_generate</code> reference-audio parameters unless a registered video provider supports audio inputs.</li>
<li>Plugins/xAI: echo PKCE challenge fields during OAuth authorization-code token exchange for xAI token-endpoint compatibility. (#83499) Thanks @fuller-stack-dev.</li>
<li>Codex app-server: hydrate current inbound image attachments before queued runs so Responses-backed agents receive Discord and other channel images as native vision input. Fixes #83466. Thanks @iannwu.</li>
<li>Codex app-server: keep native code mode available without forcing code-mode-only so OpenClaw dynamic tool turns complete through the app-server tool bridge. Fixes #83109. Thanks @daswass.</li>
<li>Release stability: recover stale session diagnostics and Codex OAuth fallback state so stuck runs and reused refresh tokens clear without blocking follow-up work. (#83503) Thanks @100yenadmin.</li>
<li>Messages/TTS: apply TTS directives before message-tool sends reach core, gateway, or plugin delivery so opt-in message-tool rooms and proactive sends attach voice notes instead of leaking raw tags. Fixes #81598. Thanks @CG-Intelligence-Agent-Jack and @CoronovirusG10.</li>
<li>Codex app-server: preserve network access for sandboxed Codex code-mode turns when the OpenClaw sandbox allows outbound egress. Fixes #83347. Thanks @YusukeIt0.</li>
<li>QA-Lab: keep the OTLP smoke decoder independent of removed OpenTelemetry generated-root internals.</li>
<li>Messages: default group/channel visible replies to automatic final delivery again, keeping <code>message_tool</code> opt-in for ambient/shared rooms and tool-reliable models.</li>
<li>CLI/TUI: force standalone <code>/exit</code> runs to terminate after <code>runTui</code> returns so onboarding-launched TUI children do not stay alive invisibly. (#83501) Thanks @fuller-stack-dev.</li>
<li>Agents/code mode: honor per-agent code-mode config in schema, runtime catalog activation, and model payload filtering. Fixes #83388. Thanks @Kaspre.</li>
<li>Agents/code mode: preserve agent, session, run, and channel context in <code>before_tool_call</code> hooks for top-level <code>exec</code>/<code>wait</code> dispatches. Fixes #83387.</li>
<li>QQBot: shorten C2C typing indicators to a 10-second window renewed every 5 seconds, capped to keep a final passive-reply slot available. (#83469)</li>
<li>Replies: keep final payload delivery after live preview updates so channels can finalize or send the completed answer instead of losing preview-only drafts. (#83468)</li>
<li>Discord: deliver final replies in progress-mode preview streams instead of deduplicating the final visible message. (#83443) Thanks @compoodment.</li>
<li>Providers/Xiaomi: replay MiMo Anthropic-compatible <code>reasoning_content</code> as provider-required thinking blocks even when OpenClaw thinking is disabled, fixing follow-up tool turns for <code>mimo-v2-flash</code>. Fixes #83407. Thanks @Xgenious7.</li>
<li>Agents/exec approvals: forward approval-runtime credentials on agent-owned Gateway approval calls so approved async commands complete through the existing runtime path instead of stalling on unauthenticated follow-up calls. Thanks @IWhatsskill, @Patrick-Erichsen, and @jesse-merhi.</li>
<li>Gateway/skills: preflight remote macOS skill-bin refreshes with a WebSocket connectivity check so stale node sessions skip quickly instead of logging slow <code>system.which</code> timeout warnings.</li>
<li>CLI/config: keep broken discovered plugins that are not referenced by active config from failing <code>openclaw config validate</code>, while preserving fatal errors for explicitly configured plugin entries.</li>
<li>GitHub Copilot: drop unsafe native Responses reasoning replay items with non-replayable IDs before dispatch, preventing affected Copilot sessions from failing with <code>invalid_request_body</code>. Fixes #83220. Thanks @galiniliev.</li>
<li>Agents/Codex: fail closed when an explicitly requested Codex harness is not registered instead of silently trying configured model fallbacks. Fixes #83349. Thanks @r2-vibes.</li>
<li>QA-Lab: make runtime tool coverage fail on missing required tool exercise instead of treating pass/pass parity envelope drift as missing coverage.</li>
<li>Core/plugins: harden clawpatch-reported edge cases across gateway auth cleanup, Claude session id paths, plugin activation policy, apply-patch hunk handling, diagnostic redaction, and plugin metadata validation.</li>
<li>UI: show reasoning choices as plain labels instead of leaking internal override wording in session and chat pickers.</li>
<li>Mac app: avoid repeating the Configuration heading inside channel quick settings.</li>
<li>Mac app: keep the Settings sidebar always visible and remove the redundant titlebar hide/show control.</li>
<li>Mac app: prefer explicit private/Tailscale/LAN Gateway endpoints over SSH tunnels, preserve legacy loopback tunnel configs, persist transport choices, and show captured SSH stderr when tunneling really fails.</li>
<li>Gateway/sessions: keep ACP/acpx and runtime child sessions visible in configured-only session lists when their owner or parent session belongs to a configured agent.</li>
<li>Mac app: keep app-level menu commands and Dashboard failure states reachable when the remote Gateway is disconnected.</li>
<li>Mac app: allow longer Gateway and Context errors to wrap in the menu instead of truncating the useful failure detail.</li>
<li>Mac app: tighten remote Gateway fields in Settings so the Connection pane keeps readable labels and full action button text.</li>
<li>Mac app: keep custom Settings card rows left-aligned and full-width so Discovery and status sections no longer appear centered or detached.</li>
<li>Mac app: align Location permission controls to the same trailing column as the rest of Settings.</li>
<li>Mac app: add Dashboard, Chat, Canvas, and Settings shortcuts to the Dock icon menu.</li>
<li>Mac app: replace the Settings window's native split-view sidebar with an explicit layout so page content keeps its leading gutter when the sidebar is shown or hidden.</li>
<li>Mac app: render channel quick config as aligned Settings rows and hide schema-only variants that cannot be edited safely from the quick pane.</li>
<li>Gateway/webchat: hide internal runtime-context and other <code>display: false</code> transcript messages from Chat history and live message events. Fixes #83216. Thanks @EmpireCreator.</li>
<li>CLI/help: keep <code>gateway</code>, <code>doctor</code>, <code>status</code>, and <code>health</code> help registration out of action/runtime imports so subcommand <code>--help</code> stays lightweight in constrained terminals. Fixes #83228. Thanks @dfguerrerom.</li>
<li>Cron/Discord: keep explicit announce runs in message-tool-only source-reply mode so scheduled agent turns post once instead of also echoing through automatic visible replies. Fixes #83261. Thanks @Theralley.</li>
<li>Telegram: preserve forum-topic origin targets in inbound, audio-preflight, and skipped-message hook contexts so follow-up delivery stays bound to the originating topic. Fixes #83302. Thanks @M00zyx.</li>
<li>Telegram: retry HTTP 421 Misdirected Request send failures on a fresh fallback transport so transient edge-node routing errors no longer drop outbound replies. Fixes #48892. (#48908) Thanks @MarsDoge.</li>
<li>Telegram: fail topic sends closed when Telegram reports <code>message thread not found</code> instead of retrying without <code>message_thread_id</code> into the base chat. Refs #83302.</li>
<li>Config/subagents: remove ignored agent-model <code>timeoutMs</code> keys, keep subagent model config to primary/fallback selection, and clean shipped stale config through doctor. Fixes #83291. Thanks @giodl73-repo.</li>
<li>Mac app: align the Sessions settings pane with the standard Settings page gutter and row spacing.</li>
<li>OpenAI/Codex: stop rejecting available <code>openai-codex</code> GPT-5.1, GPT-5.2, and GPT-5.3 model refs during config validation, while keeping removed Spark aliases suppressed. Fixes #83303.</li>
<li>Plugins/xAI: complete OAuth-backed xAI login and sidecar auth fixes, including guarded loopback callback CORS handling, video generation polling/defaults, and native-host User-Agent attribution. (#83322) Thanks @Jaaneek.</li>
<li>Codex app-server: preserve streamed native command output in mirrored transcripts and trajectory exports when final snapshots omit aggregated output. (#83200) Thanks @rozmiarD.</li>
<li>Codex app-server: fail closed when chat or sender policy denies tools, disabling native code, app, environment, and user MCP surfaces for restricted turns. (#82374) Thanks @VACInc.</li>
<li>Codex app-server: keep recent context-engine messages when oversized projected history is truncated, so short follow-ups in long channel sessions do not fall back to stale earlier turns. (#83127) Thanks @VACInc.</li>
<li>Codex app-server: keep OpenClaw session spawning searchable while steering Codex-native delegation through native subagents, avoiding duplicate direct subagent surfaces. (#83329) Thanks @fuller-stack-dev.</li>
<li>Codex app-server: recover stale childless Codex-native subagent task mirrors during maintenance and allow their registry rows to be cancelled without an OpenClaw child session. (#82836) Thanks @yshimadahrs-ship-it and @joshavant.</li>
<li>Feishu: return bound subagent delivery origins from session thread setup so Feishu subagent completions route back to the same DM or topic. (#83190) Thanks @100menotu001.</li>
<li>CLI/update: tailor post-update Gateway recovery hints by platform, showing systemd, LaunchAgent, Scheduled Task, or generic service-manager guidance instead of macOS-only recovery text. (#83096) Thanks @rubencu.</li>
<li>Plugins: apply a default 15-second timeout to legacy <code>before_agent_start</code> hooks so hung plugin handlers no longer block agent startup. Fixes #48534. (#83136) Thanks @therahul-yo.</li>
<li>Feishu: refresh inbound session delivery context for DM, group, and broadcast turns so later replies do not inherit stale WebChat routing. Fixes #78274.</li>
<li>Agents/subagents: require the initial subagent registry save before reporting spawn accepted, returning a spawn error instead of losing an untracked run when the registry write fails. (#83146) Thanks @yetval.</li>
<li>QA-Lab/qa-channel: attach redacted agent tool-start traces to outbound <code>QaBusMessage</code> records so scenarios can assert actual tool use instead of relying only on reply text. Fixes #67637. Thanks @100yenadmin.</li>
<li>QA-Lab: fail live runtime parity reports when assistant-message usage is missing, preventing <code>0 vs 0</code> live token rows from being reported as passing proof. Fixes #80411. Thanks @100yenadmin.</li>
<li>QA-Lab: add a runtime token-efficiency sidecar report that classifies Codex savings separately from regressions and fails only positive Codex-over-Pi live token deltas above threshold. Fixes #81093. Thanks @100yenadmin.</li>
<li>QA-Lab: fail Codex-backed OpenAI live runtime-pair runs before launching isolated workers when no portable Codex auth is available, while staging API-key fallbacks and configured Codex keys for isolated QA agents. Fixes #80412. Thanks @100yenadmin.</li>
<li>QA-Lab: refresh parity gates, mock frontier fixtures, model scenarios, and workflow artifact lanes to compare GPT-5.5 against Claude Opus 4.7. Fixes #74262. Thanks @100yenadmin.</li>
<li>QA-Lab: make mock parity dispatch provider-aware for source discovery and subagent scenarios so OpenAI and Anthropic lanes no longer share identical canned plans. Fixes #64879. Thanks @100yenadmin.</li>
<li>QA-Lab: stop returning Control UI bearer tokens from unauthenticated bootstrap payloads and bind Docker harness ports to loopback-only host addresses. (#66355) Thanks @pgondhi987.</li>
<li>Mac app: avoid a SwiftUI metadata crash when rendering the Cron Jobs settings pane.</li>
<li>Agents/subagents: preserve run-mode keep subagent registry entries past the session sweep TTL, so kept subagent runs remain visible after cleanup completes. Fixes #83132. (#83168) Thanks @yetval.</li>
<li>Agents/OpenAI streams: yield via <code>setTimeout(0)</code> instead of <code>setImmediate</code> between bursty Responses chunks so abort timers can fire during the yield, keeping cancel-on-timeout responsive on hot streams. Refs #82462.</li>
<li>Agents/Codex: keep legacy <code>oauthRef</code>-backed OAuth profiles usable while <code>openclaw doctor --fix</code> migrates them back to inline credentials, without creating new sidecar credentials. (#83312) Thanks @joshavant.</li>
<li>Agents/Codex: load the selected provider owner alongside the Codex harness runtime so <code>openai-codex</code> models resolve when plugin allowlists scope runtime loading. Fixes #83380. (#83519) Thanks @joshavant.</li>
<li>Telegram: fail stalled isolated-ingress handlers into tombstones and abort same-lane reply work before restarting, so later same-chat updates drain after a hung turn. Fixes #83272. (#83505) Thanks @joshavant.</li>
<li>CLI/config: send SecretRef diagnostics to stderr so JSON command stdout remains parseable.</li>
<li>CLI/doctor: seed Control UI allowed origins when migrating legacy non-loopback gateway bind host aliases like <code>0.0.0.0</code>. Fixes #83286. Thanks @giodl73-repo.</li>
<li>CLI/plugins: ship the bundled memory CLI as a package entry so package-installed <code>openclaw memory</code> commands register correctly.</li>
<li>CLI/update: defer doctor-time plugin package installs during package swaps and seed post-core repair from the updated install registry, preventing duplicate reinstall failures.</li>
<li>CLI/update: preserve old-parent-readable config metadata during legacy package handoffs, fall back only to official <code>@openclaw/*</code> npm plugin packages when ClawHub plugin artifacts are unavailable, and keep managed service package roots authoritative during updates.</li>
<li>Feishu: detect SecretRef top-level credentials as a configured default account instead of treating object-backed app secrets as missing.</li>
<li>Gateway/restart: keep ordinary unmanaged SIGUSR1/config restarts in-process instead of detach-spawning an orphaned child, preserving custom supervisor PID tracking while leaving update restarts on the fresh-process path. Fixes #65668.</li>
<li>CLI/completion: resolve concrete PowerShell profile paths and reload commands during setup and doctor completion installation. Fixes #44296. (#83059) Thanks @yu-xin-c.</li>
<li>Telegram: keep isolated long polling below the hard <code>getUpdates</code> request guard so idle bot accounts with high <code>timeoutSeconds</code> do not false-disconnect and restart-loop. Fixes #83264. Thanks @riccodecarvalho.</li>
<li>Providers/Google: preserve and recover Gemini 3 tool-call thought signatures during native replay so function-calling turns no longer fail with missing <code>thought_signature</code> 400s. Fixes #72879. (#80358) Thanks @abnershang.</li>
<li>Telegram: skip transcript-only delivery mirrors and gateway-injected rows when resolving latest assistant text, preventing retained previews from replacing final replies with stale fragments. Fixes #83159. (#83362) Thanks @joshavant.</li>
<li>Memory/QMD: keep lexical search on raw hyphenated queries while normalizing semantic QMD sub-searches, avoiding fallback to the builtin index for dashed identifiers and dates. Fixes #81328.</li>
<li>Memory-core: distinguish sqlite-vec load failures from missing semantic vector embeddings in degraded <code>memory index</code> warnings, so vector recall diagnostics point at unresolved dimensions instead of blaming sqlite-vec when the store is ready. Fixes #75624. (#83056) Thanks @xuruiray and @Noah3521.</li>
<li>Agents/subagents: preserve sandbox-peer controller ownership while routing completion announcements back to the originating run session, keeping subagent control and completion delivery scoped correctly. Fixes #80201. (#80242) Thanks @Jerry-Xin.</li>
<li>Gateway: continue restarting remaining channels when one hot-reload channel restart fails, while still reporting aggregate reload failure and rolling back plugin pre-replace stops. Fixes #83054. Thanks @zqchris.</li>
<li>Telegram: keep hot-reload restarts from marking polling accounts manually stopped and restart isolated ingress cleanly after worker shutdown, preserving Telegram replies across config reloads. Fixes #83008. (#83410) Thanks @joshavant.</li>
<li>Telegram/Ollama: pass current Telegram image attachments into native PI/Ollama vision turns so live photo prompts reach Ollama as native images. Fixes #83023. (#83516) Thanks @joshavant.</li>
<li>Gateway/secrets: split the lightweight secrets runtime state and auth-store cache from the full secrets runtime and take a startup fast path when the gateway startup config has no SecretRef values, speeding up secrets startup while preserving cleanup and refresh semantics.</li>
<li>Codex app-server: rotate oversized native Codex threads before resume and cap dynamic tool-result text entering native Codex sessions, preventing stale oversized context from surviving OpenClaw compaction. (#82981) Thanks @hansolo949.</li>
<li>Gateway/restart: drain pending replies and active chat runs during restart shutdown before sockets and channels close, aborting timed-out chat runs through the normal cleanup path. (#69121) Thanks @alexlomt.</li>
<li>Agents/Codex: use the Codex runtime context window for OpenAI-model preflight compaction and memory flush checks, so GPT-5.5 Codex sessions compact before hitting the smaller native context limit. Fixes #82982. Thanks @vliuyt.</li>
<li>QA-Lab: clean orphaned gateway temp roots when a suite parent exits and wait on gateway plus transport readiness after config restarts, reducing stale <code>qa-channel</code> noise from interrupted runs. Fixes #65506. Thanks @100yenadmin.</li>
<li>QA-Lab: wake qa-bus long polls that arrive with stale future cursors after a bus restart, preserving reconnect readiness for harness clients. (#67142) Thanks @hxy91819.</li>
<li>QA-Lab: stage Multipass transfer scripts under OpenClaw's preferred temp root instead of raw OS temp paths, keeping the VM runner inside temp-path guardrails. (#64098) Thanks @ImLukeF.</li>
<li>Agents/replies: keep surviving reply media and append a warning when other media references fail, so partial media normalization no longer drops failures silently. Thanks @Jerry-Xin.</li>
<li>Config/models: accept <code>thinkingFormat: "together"</code> in model compat config so Together routes can opt into the Together-specific thinking response shape.</li>
<li>Plugins/tokenjuice: bump the bundled tokenjuice runtime to 0.7.1, bringing Codex hook approval compatibility, pre-tool command wrapping fixes, and Rolldown/Vitest output compaction improvements into the OpenClaw plugin.</li>
<li>Agents/OpenAI: stop post-processing GPT-5 final replies with hardcoded brevity caps, preserving full channel responses instead of appending synthetic ellipses, and log when strict-agentic GPT-5 execution activates. Fixes #82910.</li>
<li>Mac app: refine the Settings General and Connection panes with cleaner status panels, card rows, and a single native titlebar sidebar toggle.</li>
<li>Agents/media: deliver failed async image, music, and video generation completions directly when requester-session completion handoff fails, so channel users see provider errors instead of silent fallback stalls.</li>
<li>Browser/CDP: keep loopback proxy bypass active across both <code>NO_PROXY</code> casings and redact home-relative Chrome MCP profile paths in attach-failure diagnostics.</li>
<li>Agents/music: steer song, jingle, beat, anthem, and instrumental requests toward <code>music_generate</code> audio creation instead of lyric-only replies, and reserve <code>lyrics</code> for exact sung words.</li>
<li>Codex app-server: record native Codex tool calls and results into trajectory artifacts so debug/trajectory exports capture the full Codex-native tool history, not just OpenClaw-bridged turns. Thanks @vyctorbrzezowski.</li>
<li>Codex/app-server: keep bound conversation sessions on the owning agent runtime so native Codex control and follow-up turns do not fall back to the default agent client. Fixes #82954. (#82993)</li>
<li>CLI/infer: run gateway model probes in fresh explicit sessions so one-shot provider checks do not inherit default agent transcript state. (#82861) Thanks @Kaspre.</li>
<li>Providers/Together: send video-generation requests to Together's v2 video API even when shared text-model config still points at the v1 base URL. (#82992)</li>
<li>Browser CLI: preserve browser-level options on nested commands, skip option values during lazy command registration, and keep long-running wait/download/dialog hooks open for their advertised wait window.</li>
<li>CLI/sessions: accept <code>openclaw sessions list</code> as an alias for <code>openclaw sessions</code>, matching other list-style commands. Fixes #81139. (#81163) Thanks @YB0y.</li>
<li>Channels/stream previews: widen compact progress draft lines and cut prose at word boundaries while preserving command/path suffixes, with <code>streaming.progress.maxLineChars</code> for channel-specific tuning.</li>
<li>CLI/plugins: have <code>openclaw plugins doctor</code> warn when a configured runtime needs a missing owner plugin, sharing the same install mapping as <code>openclaw doctor --fix</code>. Fixes #81326. (#81674) Thanks @Zavianx.</li>
<li>Agents/Codex: route OpenAI runs that resolve to <code>openai-codex</code> through the Codex provider and bootstrap OpenClaw's stored OAuth profile into the Codex harness when the harness owns transport, so <code>openai/*</code> model refs no longer fail with <code>No API key found for openai-codex</code> despite an existing Codex OAuth profile. (#82864) Thanks @ragesaq.</li>
<li>Agents/ACP: distinguish prompt-submitted and runtime-active child stalls from true interactive waits, including redacted proxy-env diagnostics for Codex ACP no-output runs. Fixes #44810.</li>
<li>Agents/memory: explain that memory-triggered compaction exposes only <code>read</code> and append-only <code>write</code> when configured core tools are unavailable in <code>tools.allow</code> warnings. Fixes #82941. Thanks @galiniliev.</li>
<li>Agents/OpenAI: preserve deterministic tool payload ordering for prompt-cache reuse across OpenAI Responses and chat completions calls. (#82940) Thanks @galiniliev.</li>
<li>ACP/Codex: honor terminal ACP turn results so failed Codex/acpx runs are not recorded as successful after only progress text. Fixes #79522. Thanks @dudaefj.</li>
<li>Telegram: warn when a media group drops photos that fail to download, including albums where every photo is skipped. Fixes #55216. (#82987) Thanks @eldar702.</li>
<li>Agents/skills: apply the full effective tool policy pipeline to inline <code>command-dispatch: tool</code> skill dispatch before owner-only filtering, preserving configured allow, deny, sandbox, sender, group, and subagent restrictions. (#78525)</li>
<li>Codex: avoid spawning native hook relay subprocesses for post-tool/finalize events with no registered hook handlers while preserving pre-tool safety and approval relays. Fixes #76552. (#78004) Thanks @evgyur.</li>
<li>Channel accounts: keep top-level default channel accounts visible when named accounts are added alongside default credential material, so mixed legacy/new account configs keep resolving <code>default</code> instead of silently dropping it.</li>
<li>Agents/CLI: reject empty successful CLI subprocess replies as <code>empty_response</code> and keep them out of shared auth-profile health, so blank Claude CLI results no longer become green no-payload turns. Fixes #83231. (#83421) Thanks @joshavant.</li>
<li>Codex/Telegram: synthesize native Codex tool progress from final turn snapshots so Telegram <code>/verbose</code> stays visible when command events arrive only at completion.</li>
<li>Codex/Telegram: deliver Codex verbose tool summaries in direct message-tool-only turns while suppressing message-send and activity-log noise. (#83186) Thanks @kurplunkin.</li>
<li>Mac app: make Channels settings open faster by deferring config-schema work, avoiding startup channel probes, caching decoded channel status rows, and showing only compact quick settings instead of the full generated channel schema.</li>
<li>Control UI: include the Control UI and Gateway protocol versions in protocol-mismatch errors so stale app/dashboard pairings identify which side needs rebuilding or restarting.</li>
<li>Gateway/protocol: restore Gateway WS protocol v4 and keep <code>message.action</code> room-event metadata on the existing <code>inboundTurnKind</code> wire field while preserving internal inbound-event classification.</li>
<li>Agents/tools: prefer non-webchat session-key routes when the message tool has stale webchat context, so message-tool-only replies keep delivering to the originating channel. Fixes #82911. (#83004) Thanks @joshavant.</li>
<li>Channels: keep direct-message last-route writes on isolated <code>per-channel-peer</code> sessions instead of contaminating the agent main session with channel delivery context. Fixes #36614. Thanks @aspenas.</li>
<li>Mac app: move the Settings sidebar toggle into the native titlebar and tighten the General pane width.</li>
<li>Mac app: keep visited Settings panes mounted so switching tabs no longer blanks and reloads their content.</li>
<li>Mac app: make Config settings open from shallow schema lookups and load selected paths on demand instead of fetching and rendering the full generated config schema up front.</li>
<li>Codex: sanitize inline image payloads before Codex app-server and OpenAI Responses replay, and clear poisoned Codex thread bindings after invalid image errors. Fixes #82878.</li>
<li>Providers/GitHub Copilot: request identity-encoded Copilot API responses across token exchange, catalog, model calls, usage, and embeddings so compressed Business-account error payloads no longer reach JSON parsers as gzip bytes. Fixes #82871. Thanks @tonyfe01.</li>
<li>Telegram: redact nested raw-update identifiers and user metadata before verbose raw update logging, preserving useful update/message ids without exposing chat, user, command, or profile details. (#82945) Thanks @galiniliev and @joshavant.</li>
<li>Telegram: preserve replied-to bot messages, captions, and media metadata in group reply chains so follow-up replies understand what the user is reacting to. (#82863)</li>
<li>Providers/Together: update PI runtime packages to 0.74.1 and emit Together-style <code>reasoning.enabled</code>/<code>max_tokens</code> controls for reasoning-capable OpenAI-completions models.</li>
<li>Agents/diagnostics: split slow embedded-run <code>attempt-dispatch</code> startup summaries into workspace, prompt, runtime-plan, and final dispatch subspans so traces identify the delayed setup phase. Fixes #82782. (#82783) Thanks @galiniliev.</li>
<li>Agents/Codex: flatten nested tool-result middleware blocks into bounded text so successful message sends are no longer replaced with <code>Tool output unavailable due to post-processing error</code>. Fixes #82912. Thanks @joeykrug.</li>
<li>CLI/media: accept HTTP(S) URLs in <code>openclaw infer image describe --file</code>, fetching remote images through the guarded media path instead of treating URLs as local files. Fixes #82837. (#82854) Thanks @neeravmakwana.</li>
<li>Agents/subagents: keep session-backed parent runs active when the child wait call times out before the child session has actually settled, so late subagent completions are reconciled instead of being lost. Fixes #82787. Thanks @ramitrkar-hash.</li>
<li>Control UI: advertise shared Gateway protocol constants in browser connect frames, fixing protocol mismatch handshakes after protocol constant drift. Fixes #82882. Thanks @galiniliev.</li>
<li>Gateway: add rollback protocol-mismatch diagnostics, including client protocol ranges in Gateway logs and deep status/doctor hints for stale client processes. Fixes #82841. (#82908)</li>
<li>Agents/subagents: keep successful keep-mode completion payloads pending after final-delivery retry exhaustion, so requester recovery no longer loses final subagent results. Fixes #82583. (#82999) Thanks @joshavant.</li>
<li>Gateway/auth: allow same-host trusted-proxy callers to use the documented local direct <code>gateway.auth.password</code> fallback after revisiting the #78684 fail-closed policy, while keeping token fallback rejected and forwarded-header requests on the trusted-proxy path. Fixes #82607. (#82953) Thanks @joshavant.</li>
<li>Agents/subagents: wait for queued completion handoffs to reach the parent transcript before marking them announced, preventing busy parent runs from cleaning up before observing child results. Fixes #82913. (#83039) Thanks @joshavant.</li>
<li>Agents/subagents: route group/channel subagent completions through message-tool-only handoffs when required and keep active-requester wake failures from dropping completion delivery. Fixes #82803. Thanks @galiniliev, @yozakura-ava, and @moeedahmed.</li>
<li>Memory-core: scan persisted memory source sessions on startup, comparing on-disk transcripts against the index and marking only missing/newer/resized files dirty for incremental sync. Fixes #82341. (#82341) Thanks @giodl73-repo.</li>
<li>Telegram: keep the top-level default account in the account list when named accounts or bindings are added alongside top-level credentials, preserving default polling while still letting named-only configs resolve to a single account. Fixes #82794. (#82794) Thanks @giodl73-repo.</li>
<li>CLI/models: reuse command-scoped plugin metadata across model listing, provider catalog, auth, and synthetic-auth checks, restoring fast <code>openclaw models</code> runs for plugin-heavy installs. Fixes #82881. (#83033) Thanks @joshavant.</li>
<li>CLI/channels: show configured official external channels such as Discord in <code>openclaw channels list</code> when their plugin package is missing, including the install and doctor repair command instead of reporting no configured channels. Fixes #82813.</li>
<li>Signal: preserve mixed-case group IDs through routing and session persistence so group auto-replies keep delivering after updates. Fixes #82827.</li>
<li>Agents/tools: keep the <code>message</code> tool available in embedded runs when it is explicitly allowed through <code>tools.alsoAllow</code> or runtime tool allowlists, so channel plugins with custom reply delivery can still use configured message sends. Fixes #82833. Thanks @cn1313113.</li>
<li>WhatsApp: honor forced document delivery for outbound image, GIF, and video media so <code>forceDocument</code>/<code>asDocument</code> sends preserve original media bytes instead of using compressed media payloads. (#79272) Thanks @itsuzef.</li>
<li>WhatsApp: name outbound document attachments from their MIME type when no filename is provided, so PDF and CSV sends arrive as <code>file.pdf</code> and <code>file.csv</code> instead of an extensionless <code>file</code>. Thanks @mcaxtr.</li>
<li>Process/diagnostics: report active lane blockers in lane wait warnings so <code>queueAhead=0</code> no longer hides commands waiting behind active work. Fixes #82791. (#82792) Thanks @galiniliev.</li>
<li>Process/diagnostics: stop counting the active processing turn as queued backlog in liveness warnings so transient max-only event-loop spikes do not surface as gateway warnings.</li>
<li>Agents/replies: classify provider conversation-state rejections and return a clear message-channel error instead of auto-resetting or falling back to a generic runner failure. (#82616) Thanks @dutifulbob.</li>
<li>Browser plugin: trust managed Chrome CDP diagnostics when launch HTTP probes race cold-start readiness, avoiding false startup failures. Fixes #82904. (#82986) Thanks @kmanan and @hclsys.</li>
<li>Android: prompt before replacing a changed Gateway TLS thumbprint, showing the old and new SHA-256 fingerprints so users can accept expected certificate rotations instead of hard failing on pin mismatch. (#83077) Thanks @sliekens.</li>
<li>CLI/status: render extra gateway-like service diagnostics as warning/info output instead of error output. Fixes #46930. (#82922) thanks @giodl73-repo.</li>
<li>Agents/failover: classify Moonshot/Kimi exhausted-balance HTTP 429 payloads as billing instead of generic rate limits, preserving billing guidance and fallback behavior. Fixes #43447. (#83079) Thanks @leno23.</li>
<li>Plugin SDK: bundle <code>openclaw/plugin-sdk/zod</code> into the published package artifact and verify the packed zod subpath stays self-contained, so pnpm global installs can register plugins without a package-local <code>zod</code> symlink. Fixes #78398. (#78515) Thanks @ggzeng.</li>
<li>Providers/Google: drop compaction-truncated Gemini thought signatures before replay so malformed Base64 no longer aborts the next assistant turn. (#82995) Thanks @wAngByg.</li>
</ul>
<p><a href="https://github.com/openclaw/openclaw/blob/main/CHANGELOG.md">View full changelog</a></p>
]]></description>
<enclosure url="https://github.com/openclaw/openclaw/releases/download/v2026.5.18/OpenClaw-2026.5.18.zip" length="53924201" type="application/octet-stream" sparkle:edSignature="cU0TfUmBZbVOpgwou+GS7RQiDhEGVUxjK+bwsl1RXiqvJi9ErsYebZIxVayH8++v5PeycoK5+LQF5gLiXQa2AA=="/>
</item>
</channel>
</rss>

View File

@@ -253,12 +253,13 @@ Pre-req checklist:
5) Grant runtime permissions for capabilities you expect to pass (camera/mic/location/notification listener/location, etc.).
6) No interactive system dialogs should be pending before test start.
7) Canvas host is enabled and reachable from the device (do not run gateway with `OPENCLAW_SKIP_CANVAS_HOST=1`; startup logs should include `canvas host mounted at .../__openclaw__/`).
8) Local operator test client pairing is approved. If first run fails with `pairing required`, approve latest pending device pairing request, then rerun:
8) Local operator test client pairing is approved. If first run fails with `pairing required`, preview the latest pending request, approve the printed request ID, then rerun:
9) For A2UI checks, keep the app on **Screen** tab; the node now auto-refreshes canvas capability once on first A2UI reachability failure (TTL-safe retry).
```bash
openclaw devices list
openclaw devices approve --latest
openclaw devices approve --latest # preview only; copy the requestId from output
openclaw devices approve <requestId>
```
Run:
@@ -284,7 +285,7 @@ What it does:
Common failure quick-fixes:
- `pairing required` before tests start:
- approve pending device pairing (`openclaw devices approve --latest`) and rerun.
- list pending requests (`openclaw devices list`), then approve with the exact ID (`openclaw devices approve <requestId>`) and rerun.
- `A2UI host not reachable` / `A2UI_HOST_NOT_CONFIGURED`:
- ensure the Canvas plugin host is running and reachable, keep the app on the **Screen** tab. The app refreshes the Canvas plugin surface URL once before failing; if it still fails, reconnect app and rerun.
- `NODE_BACKGROUND_UNAVAILABLE: canvas unavailable`:

View File

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

View File

@@ -32,6 +32,8 @@ class MainViewModel(
private var foreground = true
private val _requestedHomeDestination = MutableStateFlow<HomeDestination?>(null)
val requestedHomeDestination: StateFlow<HomeDestination?> = _requestedHomeDestination
private val _startOnboardingAtGatewaySetup = MutableStateFlow(false)
val startOnboardingAtGatewaySetup: StateFlow<Boolean> = _startOnboardingAtGatewaySetup
private val _chatDraft = MutableStateFlow<String?>(null)
val chatDraft: StateFlow<String?> = _chatDraft
private val _pendingAssistantAutoSend = MutableStateFlow<String?>(null)
@@ -159,6 +161,7 @@ class MainViewModel(
val chatSessionKey: StateFlow<String> = runtimeState(initial = "main") { it.chatSessionKey }
val chatSessionId: StateFlow<String?> = runtimeState(initial = null) { it.chatSessionId }
val chatMessages: StateFlow<List<ChatMessage>> = runtimeState(initial = emptyList()) { it.chatMessages }
val chatHistoryLoading: StateFlow<Boolean> = runtimeState(initial = false) { it.chatHistoryLoading }
val chatError: StateFlow<String?> = runtimeState(initial = null) { it.chatError }
val chatHealthOk: StateFlow<Boolean> = runtimeState(initial = false) { it.chatHealthOk }
val chatThinkingLevel: StateFlow<String> = runtimeState(initial = "off") { it.chatThinkingLevel }
@@ -262,6 +265,17 @@ class MainViewModel(
prefs.setOnboardingCompleted(value)
}
fun pairNewGateway() {
runtimeRef.value?.disconnect()
resetGatewaySetupAuth()
_startOnboardingAtGatewaySetup.value = true
prefs.setOnboardingCompleted(false)
}
fun clearGatewaySetupStartRequest() {
_startOnboardingAtGatewaySetup.value = false
}
fun setCanvasDebugStatusEnabled(value: Boolean) {
prefs.setCanvasDebugStatusEnabled(value)
}

View File

@@ -490,7 +490,6 @@ class NodeRuntime(
scope = scope,
session = operatorSession,
json = json,
supportsChatSubscribe = false,
).also {
it.applyMainSessionKey(_mainSessionKey.value)
}
@@ -502,7 +501,6 @@ class NodeRuntime(
context = appContext,
scope = scope,
session = operatorSession,
supportsChatSubscribe = false,
isConnected = { operatorConnected },
onBeforeSpeak = { micCapture.pauseForTts() },
onAfterSpeak = { micCapture.resumeAfterTts() },
@@ -610,7 +608,6 @@ class NodeRuntime(
context = appContext,
scope = scope,
session = operatorSession,
supportsChatSubscribe = true,
isConnected = { operatorConnected },
onBeforeSpeak = { micCapture.pauseForTts() },
onAfterSpeak = { micCapture.resumeAfterTts() },
@@ -858,6 +855,7 @@ class NodeRuntime(
val chatSessionKey: StateFlow<String> = chat.sessionKey
val chatSessionId: StateFlow<String?> = chat.sessionId
val chatMessages: StateFlow<List<ChatMessage>> = chat.messages
val chatHistoryLoading: StateFlow<Boolean> = chat.historyLoading
val chatError: StateFlow<String?> = chat.errorText
val chatHealthOk: StateFlow<Boolean> = chat.healthOk
val chatThinkingLevel: StateFlow<String> = chat.thinkingLevel
@@ -1150,7 +1148,7 @@ class NodeRuntime(
NodeForegroundService.setVoiceCaptureMode(appContext, VoiceCaptureMode.TalkMode)
talkMode.ttsOnAllResponses = true
talkMode.setPlaybackEnabled(speakerEnabled.value)
talkMode.ensureChatSubscribed()
talkMode.refreshConfig()
externalAudioCaptureActive.value = true
}
@@ -1222,7 +1220,7 @@ class NodeRuntime(
}
// Tapping mic on interrupts any active TTS (barge-in).
stopVoicePlayback()
scope.launch { talkMode.ensureChatSubscribed() }
scope.launch { talkMode.refreshConfig() }
micCapture.setMicEnabled(true)
externalAudioCaptureActive.value = true
}
@@ -1235,7 +1233,7 @@ class NodeRuntime(
NodeForegroundService.setVoiceCaptureMode(appContext, VoiceCaptureMode.TalkMode)
talkMode.ttsOnAllResponses = true
talkMode.setPlaybackEnabled(speakerEnabled.value)
scope.launch { talkMode.ensureChatSubscribed() }
scope.launch { talkMode.refreshConfig() }
talkMode.setEnabled(true)
externalAudioCaptureActive.value = true
}
@@ -1446,7 +1444,7 @@ class NodeRuntime(
endpoint: GatewayEndpoint,
auth: GatewayConnectAuth,
) {
if (operatorConnected || operatorStatusText == "Connecting…") {
if (operatorConnected) {
return
}
val operatorAuth =

View File

@@ -17,12 +17,12 @@ import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.buildJsonObject
import java.util.UUID
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicLong
class ChatController(
private val scope: CoroutineScope,
private val session: GatewaySession,
private val json: Json,
private val supportsChatSubscribe: Boolean,
) {
private var appliedMainSessionKey = "main"
private val _sessionKey = MutableStateFlow("main")
@@ -34,6 +34,9 @@ class ChatController(
private val _messages = MutableStateFlow<List<ChatMessage>>(emptyList())
val messages: StateFlow<List<ChatMessage>> = _messages.asStateFlow()
private val _historyLoading = MutableStateFlow(false)
val historyLoading: StateFlow<Boolean> = _historyLoading.asStateFlow()
private val _errorText = MutableStateFlow<String?>(null)
val errorText: StateFlow<String?> = _errorText.asStateFlow()
@@ -60,25 +63,27 @@ class ChatController(
private val pendingRunTimeoutJobs = ConcurrentHashMap<String, Job>()
private val optimisticMessagesByRunId = LinkedHashMap<String, ChatMessage>()
private val pendingRunTimeoutMs = 120_000L
private val historyLoadGeneration = AtomicLong(0)
private var lastHealthPollAtMs: Long? = null
fun onDisconnected(message: String) {
_healthOk.value = false
// Not an error; keep connection status in the UI pill.
_errorText.value = null
clearPendingRuns()
pendingToolCallsById.clear()
publishPendingToolCalls()
_streamingAssistantText.value = null
_historyLoading.value = false
_sessionId.value = null
}
fun load(sessionKey: String) {
val key = normalizeRequestedSessionKey(sessionKey)
_sessionKey.value = key
optimisticMessagesByRunId.clear()
scope.launch { bootstrap(forceHealth = true, refreshSessions = true) }
val generation = beginHistoryLoad(key, clearMessages = key != _sessionKey.value)
scope.launch {
bootstrap(sessionKey = key, generation = generation, forceHealth = true, refreshSessions = true)
}
}
fun applyMainSessionKey(mainSessionKey: String) {
@@ -92,12 +97,23 @@ class ChatController(
)
appliedMainSessionKey = nextState.appliedMainSessionKey
if (_sessionKey.value == nextState.currentSessionKey) return
_sessionKey.value = nextState.currentSessionKey
scope.launch { bootstrap(forceHealth = true, refreshSessions = true) }
val generation = beginHistoryLoad(nextState.currentSessionKey, clearMessages = true)
scope.launch {
bootstrap(
sessionKey = nextState.currentSessionKey,
generation = generation,
forceHealth = true,
refreshSessions = true,
)
}
}
fun refresh() {
scope.launch { bootstrap(forceHealth = true, refreshSessions = true) }
val key = normalizeRequestedSessionKey(_sessionKey.value)
val generation = beginHistoryLoad(key, clearMessages = false)
scope.launch {
bootstrap(sessionKey = key, generation = generation, forceHealth = true, refreshSessions = true)
}
}
fun refreshSessions(limit: Int? = null) {
@@ -114,11 +130,30 @@ class ChatController(
val key = normalizeRequestedSessionKey(sessionKey)
if (key.isEmpty()) return
if (key == _sessionKey.value) return
val generation = beginHistoryLoad(key, clearMessages = true)
scope.launch {
bootstrap(sessionKey = key, generation = generation, forceHealth = true, refreshSessions = false)
}
}
private fun beginHistoryLoad(
key: String,
clearMessages: Boolean,
): Long {
val generation = historyLoadGeneration.incrementAndGet()
_sessionKey.value = key
optimisticMessagesByRunId.clear()
// Keep the thread switch path lean: history + health are needed immediately,
// but the session list is usually unchanged and can refresh on explicit pull-to-refresh.
scope.launch { bootstrap(forceHealth = true, refreshSessions = false) }
_errorText.value = null
_healthOk.value = false
clearPendingRuns()
pendingToolCallsById.clear()
publishPendingToolCalls()
_streamingAssistantText.value = null
_sessionId.value = null
_historyLoading.value = true
if (clearMessages) {
_messages.value = emptyList()
}
return generation
}
private fun normalizeRequestedSessionKey(sessionKey: String): String {
@@ -289,27 +324,22 @@ class ChatController(
}
private suspend fun bootstrap(
sessionKey: String,
generation: Long,
forceHealth: Boolean,
refreshSessions: Boolean,
) {
_errorText.value = null
_healthOk.value = false
clearPendingRuns()
pendingToolCallsById.clear()
publishPendingToolCalls()
_streamingAssistantText.value = null
_sessionId.value = null
val key = _sessionKey.value
try {
if (supportsChatSubscribe) {
session.sendNodeEvent("chat.subscribe", """{"sessionKey":"$key"}""")
}
val historyJson = session.request("chat.history", """{"sessionKey":"$key"}""")
val history = parseHistory(historyJson, sessionKey = key, previousMessages = _messages.value)
val historyJson =
session.request(
"chat.history",
buildJsonObject { put("sessionKey", JsonPrimitive(sessionKey)) }.toString(),
)
if (!isCurrentHistoryLoad(sessionKey, _sessionKey.value, generation, historyLoadGeneration.get())) return
val history = parseHistory(historyJson, sessionKey = sessionKey, previousMessages = _messages.value)
_messages.value = mergeOptimisticMessages(incoming = history.messages, optimistic = optimisticMessagesByRunId.values)
_sessionId.value = history.sessionId
_historyLoading.value = false
history.thinkingLevel
?.trim()
?.takeIf { it.isNotEmpty() }
@@ -320,7 +350,9 @@ class ChatController(
fetchSessions(limit = 50)
}
} catch (err: Throwable) {
if (!isCurrentHistoryLoad(sessionKey, _sessionKey.value, generation, historyLoadGeneration.get())) return
_errorText.value = err.message
_historyLoading.value = false
}
}
@@ -387,9 +419,29 @@ class ChatController(
_streamingAssistantText.value = null
scope.launch {
try {
val currentSessionKey = _sessionKey.value
val currentGeneration = historyLoadGeneration.get()
val historyJson =
session.request("chat.history", """{"sessionKey":"${_sessionKey.value}"}""")
val history = parseHistory(historyJson, sessionKey = _sessionKey.value, previousMessages = _messages.value)
session.request(
"chat.history",
buildJsonObject { put("sessionKey", JsonPrimitive(currentSessionKey)) }.toString(),
)
if (
!isCurrentHistoryLoad(
currentSessionKey,
_sessionKey.value,
currentGeneration,
historyLoadGeneration.get(),
)
) {
return@launch
}
val history =
parseHistory(
historyJson,
sessionKey = currentSessionKey,
previousMessages = _messages.value,
)
_messages.value = mergeOptimisticMessages(incoming = history.messages, optimistic = optimisticMessagesByRunId.values)
_sessionId.value = history.sessionId
history.thinkingLevel
@@ -527,7 +579,7 @@ class ChatController(
array.mapNotNull { item ->
val obj = item.asObjectOrNull() ?: return@mapNotNull null
val role = obj["role"].asStringOrNull() ?: return@mapNotNull null
val content = obj["content"].asArrayOrNull()?.mapNotNull(::parseMessageContent) ?: emptyList()
val content = obj["content"].asArrayOrNull()?.mapNotNull(::parseChatMessageContent) ?: emptyList()
val ts = obj["timestamp"].asLongOrNull()
ChatMessage(
id = UUID.randomUUID().toString(),
@@ -545,21 +597,6 @@ class ChatController(
)
}
private fun parseMessageContent(el: JsonElement): ChatMessageContent? {
val obj = el.asObjectOrNull() ?: return null
val type = obj["type"].asStringOrNull() ?: "text"
return if (type == "text") {
ChatMessageContent(type = "text", text = obj["text"].asStringOrNull())
} else {
ChatMessageContent(
type = type,
mimeType = obj["mimeType"].asStringOrNull(),
fileName = obj["fileName"].asStringOrNull(),
base64 = obj["content"].asStringOrNull(),
)
}
}
private fun parseSessions(jsonString: String): List<ChatSessionEntry> {
val root = json.parseToJsonElement(jsonString).asObjectOrNull() ?: return emptyList()
val sessions = root["sessions"].asArrayOrNull() ?: return emptyList()
@@ -593,6 +630,34 @@ class ChatController(
}
}
internal fun isCurrentHistoryLoad(
requestedSessionKey: String,
currentSessionKey: String,
requestGeneration: Long,
activeGeneration: Long,
): Boolean = requestedSessionKey == currentSessionKey && requestGeneration == activeGeneration
internal fun parseChatMessageContent(el: JsonElement): ChatMessageContent? {
val obj = el.asObjectOrNull() ?: return null
return when (obj["type"].asStringOrNull() ?: "text") {
"text", "input_text", "output_text" ->
ChatMessageContent(
type = "text",
text = obj["text"].asStringOrNull() ?: obj["content"].asStringOrNull(),
)
"image" ->
ChatMessageContent(
type = "image",
mimeType = obj["mimeType"].asStringOrNull(),
fileName = obj["fileName"].asStringOrNull(),
base64 = obj["content"].asStringOrNull()?.takeIf { it.isNotBlank() },
)
else -> null
}
}
internal data class MainSessionState(
val currentSessionKey: String,
val appliedMainSessionKey: String,

View File

@@ -1,12 +1,14 @@
package ai.openclaw.app.gateway
import android.util.Log
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
@@ -88,6 +90,7 @@ private data class SelectedConnectAuth(
val authDeviceToken: String?,
val authPassword: String?,
val signatureToken: String?,
val storedScopes: List<String>,
val authSource: GatewayConnectAuthSource,
val attemptedDeviceTokenRetry: Boolean,
)
@@ -384,6 +387,22 @@ class GatewaySession(
private val client: OkHttpClient = buildClient()
private var socket: WebSocket? = null
private val loggerTag = "OpenClawGateway"
private val incomingMessages = Channel<String>(Channel.UNLIMITED)
private val messagePumpJob =
scope.launch(Dispatchers.IO) {
for (text in incomingMessages) {
try {
handleMessage(text)
} catch (err: CancellationException) {
throw err
} catch (err: Throwable) {
Log.w(
loggerTag,
"gateway message handling failed: ${err.message ?: err::class.java.simpleName}",
)
}
}
}
val remoteAddress: String = formatGatewayAuthority(endpoint.host, endpoint.port)
@@ -475,6 +494,11 @@ class GatewaySession(
fun closeQuietly() {
if (isClosed.compareAndSet(false, true)) {
incomingMessages.close()
messagePumpJob.cancel()
if (!connectDeferred.isCompleted) {
connectDeferred.completeExceptionally(IllegalStateException("Gateway closed"))
}
socket?.close(1000, "bye")
socket = null
closedDeferred.complete(Unit)
@@ -519,7 +543,7 @@ class GatewaySession(
webSocket: WebSocket,
text: String,
) {
scope.launch { handleMessage(text) }
incomingMessages.trySend(text)
}
override fun onFailure(
@@ -531,6 +555,7 @@ class GatewaySession(
connectDeferred.completeExceptionally(t)
}
if (isClosed.compareAndSet(false, true)) {
incomingMessages.close()
failPending()
closedDeferred.complete(Unit)
onDisconnected("Gateway error: ${t.message ?: t::class.java.simpleName}")
@@ -546,6 +571,7 @@ class GatewaySession(
connectDeferred.completeExceptionally(IllegalStateException("Gateway closed: $reason"))
}
if (isClosed.compareAndSet(false, true)) {
incomingMessages.close()
failPending()
closedDeferred.complete(Unit)
onDisconnected("Gateway closed: $reason")
@@ -555,7 +581,8 @@ class GatewaySession(
private suspend fun sendConnect(connectNonce: String) {
val identity = identityStore.loadOrCreate()
val storedToken = deviceAuthStore.loadToken(identity.deviceId, options.role)?.trim()
val storedEntry = deviceAuthStore.loadEntry(identity.deviceId, options.role)
val storedToken = storedEntry?.token?.trim()
val selectedAuth =
selectConnectAuth(
endpoint = endpoint,
@@ -565,6 +592,7 @@ class GatewaySession(
explicitBootstrapToken = bootstrapToken?.trim()?.takeIf { it.isNotEmpty() },
explicitPassword = password?.trim()?.takeIf { it.isNotEmpty() },
storedToken = storedToken?.takeIf { it.isNotEmpty() },
storedScopes = storedEntry?.scopes.orEmpty(),
)
if (selectedAuth.attemptedDeviceTokenRetry) {
pendingDeviceTokenRetry = false
@@ -618,7 +646,6 @@ class GatewaySession(
val allowedOperatorScopes =
setOf(
"operator.approvals",
"operator.pairing",
"operator.read",
"operator.write",
)
@@ -768,6 +795,7 @@ class GatewaySession(
else -> null
}
val connectScopes = resolveConnectScopes(selectedAuth)
val signedAtMs = System.currentTimeMillis()
val payload =
DeviceAuthPayload.buildV3(
@@ -775,7 +803,7 @@ class GatewaySession(
clientId = client.id,
clientMode = client.mode,
role = options.role,
scopes = options.scopes,
scopes = connectScopes,
signedAtMs = signedAtMs,
token = selectedAuth.signatureToken,
nonce = connectNonce,
@@ -814,7 +842,7 @@ class GatewaySession(
)
}
put("role", JsonPrimitive(options.role))
if (options.scopes.isNotEmpty()) put("scopes", JsonArray(options.scopes.map(::JsonPrimitive)))
if (connectScopes.isNotEmpty()) put("scopes", JsonArray(connectScopes.map(::JsonPrimitive)))
authJson?.let { put("auth", it) }
deviceJson?.let { put("device", it) }
put("locale", JsonPrimitive(locale))
@@ -824,6 +852,16 @@ class GatewaySession(
}
}
private fun resolveConnectScopes(selectedAuth: SelectedConnectAuth): List<String> {
if (selectedAuth.authSource == GatewayConnectAuthSource.BOOTSTRAP_TOKEN) {
return filteredBootstrapHandoffScopes(options.role, options.scopes).orEmpty()
}
if (selectedAuth.authSource == GatewayConnectAuthSource.DEVICE_TOKEN && selectedAuth.storedScopes.isNotEmpty()) {
return selectedAuth.storedScopes
}
return options.scopes
}
private suspend fun handleMessage(text: String) {
val frame = json.parseToJsonElement(text).asObjectOrNull() ?: return
when (frame["type"].asStringOrNull()) {
@@ -1097,6 +1135,7 @@ class GatewaySession(
explicitBootstrapToken: String?,
explicitPassword: String?,
storedToken: String?,
storedScopes: List<String>,
): SelectedConnectAuth {
val shouldUseDeviceRetryToken =
pendingDeviceTokenRetry &&
@@ -1130,6 +1169,7 @@ class GatewaySession(
authDeviceToken = authDeviceToken,
authPassword = explicitPassword,
signatureToken = authToken ?: authBootstrapToken,
storedScopes = storedScopes,
authSource = authSource,
attemptedDeviceTokenRetry = shouldUseDeviceRetryToken,
)

View File

@@ -162,12 +162,9 @@ class ConnectionManager(
fun buildOperatorConnectOptions(): GatewayConnectOptions =
GatewayConnectOptions(
role = "operator",
// QR bootstrap hands Android a bounded operator token that includes approvals; keep the
// default operator reconnect request aligned so the post-bootstrap loop can approve work.
scopes =
listOf(
"operator.approvals",
"operator.pairing",
"operator.read",
"operator.write",
),

View File

@@ -12,7 +12,8 @@ import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.compose.LocalLifecycleOwner
import kotlinx.coroutines.delay
internal const val PAIRING_AUTO_RETRY_MS = 6_000L
internal const val PAIRING_INITIAL_AUTO_RETRY_MS = 1_500L
internal const val PAIRING_AUTO_RETRY_MS = 4_000L
@Composable
internal fun PairingAutoRetryEffect(
@@ -40,9 +41,10 @@ internal fun PairingAutoRetryEffect(
if (!enabled || !lifecycleStarted) {
return@LaunchedEffect
}
delay(PAIRING_INITIAL_AUTO_RETRY_MS)
while (true) {
delay(PAIRING_AUTO_RETRY_MS)
onRetry()
delay(PAIRING_AUTO_RETRY_MS)
}
}
}

View File

@@ -56,7 +56,7 @@ internal fun NodesDevicesSettingsScreen(
listOf(
SettingsMetric("Nodes", summary.nodes.size.toString()),
SettingsMetric("Online", summary.nodes.count { it.connected }.toString()),
SettingsMetric("Devices", if (summary.devicePairingAvailable) summary.pairedDevices.size.toString() else "Locked"),
SettingsMetric("Devices", if (summary.devicePairingAvailable) summary.pairedDevices.size.toString() else "Admin"),
SettingsMetric("Pending", summary.pendingDevices.size.toString()),
),
)
@@ -95,7 +95,7 @@ private fun NodesDevicesPanel(summary: GatewayNodesDevicesSummary) {
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
if (!summary.devicePairingAvailable) {
ClawPanel {
Text(text = "Pairing controls are not available from this connection.", style = ClawTheme.type.body, color = ClawTheme.colors.textMuted)
Text(text = "Device pairing admin needs elevated access. Connected nodes still work.", style = ClawTheme.type.body, color = ClawTheme.colors.textMuted)
}
}
if (summary.pendingDevices.isNotEmpty()) {

View File

@@ -23,6 +23,7 @@ import android.content.pm.PackageManager
import android.hardware.Sensor
import android.hardware.SensorManager
import android.os.Build
import android.os.SystemClock
import android.provider.Settings
import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
@@ -79,6 +80,7 @@ import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableLongStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
@@ -100,6 +102,7 @@ import androidx.lifecycle.compose.LocalLifecycleOwner
import com.google.mlkit.vision.barcode.common.Barcode
import com.google.mlkit.vision.codescanner.GmsBarcodeScannerOptions
import com.google.mlkit.vision.codescanner.GmsBarcodeScanning
import kotlinx.coroutines.delay
private enum class OnboardingStep {
Welcome,
@@ -108,6 +111,8 @@ private enum class OnboardingStep {
Permissions,
}
private const val GATEWAY_CONNECT_SETTLING_MS = 2_500L
@Composable
fun OnboardingFlow(
viewModel: MainViewModel,
@@ -123,6 +128,7 @@ fun OnboardingFlow(
val gateways by viewModel.gateways.collectAsState()
val savedToken by viewModel.gatewayToken.collectAsState()
val pendingTrust by viewModel.pendingGatewayTrust.collectAsState()
val startAtGatewaySetup by viewModel.startOnboardingAtGatewaySetup.collectAsState()
val ready = canFinishOnboarding(isConnected = isConnected, isNodeConnected = isNodeConnected)
var step by rememberSaveable { mutableStateOf(OnboardingStep.Welcome) }
@@ -134,6 +140,8 @@ fun OnboardingFlow(
var password by rememberSaveable { mutableStateOf("") }
var setupError by rememberSaveable { mutableStateOf<String?>(null) }
var attemptedConnect by rememberSaveable { mutableStateOf(false) }
var connectAttemptStartedAtMs by rememberSaveable { mutableLongStateOf(0L) }
var recoveryNowMs by remember { mutableLongStateOf(SystemClock.elapsedRealtime()) }
val qrScannerOptions =
remember {
@@ -146,12 +154,26 @@ fun OnboardingFlow(
val permissionState = rememberPermissionState(context = context, viewModel = viewModel)
LaunchedEffect(startAtGatewaySetup) {
if (startAtGatewaySetup) {
step = OnboardingStep.Gateway
viewModel.clearGatewaySetupStartRequest()
}
}
LaunchedEffect(ready, attemptedConnect) {
if (attemptedConnect && ready) {
step = OnboardingStep.Permissions
}
}
LaunchedEffect(step, connectAttemptStartedAtMs) {
if (step != OnboardingStep.Recovery || connectAttemptStartedAtMs <= 0L) return@LaunchedEffect
recoveryNowMs = SystemClock.elapsedRealtime()
delay(GATEWAY_CONNECT_SETTLING_MS)
recoveryNowMs = SystemClock.elapsedRealtime()
}
pendingTrust?.let { prompt ->
AlertDialog(
onDismissRequest = viewModel::declineGatewayTrustPrompt,
@@ -250,6 +272,7 @@ fun OnboardingFlow(
setupError = null
attemptedConnect = true
connectAttemptStartedAtMs = SystemClock.elapsedRealtime()
viewModel.resetGatewaySetupAuth()
viewModel.setManualEnabled(true)
viewModel.setManualHost(config.host)
@@ -275,10 +298,12 @@ fun OnboardingFlow(
remoteAddress = remoteAddress,
ready = ready,
attemptedConnect = attemptedConnect,
connectSettling = recoveryNowMs - connectAttemptStartedAtMs < GATEWAY_CONNECT_SETTLING_MS,
onAutoRetry = viewModel::refreshGatewayConnection,
onBack = { step = OnboardingStep.Gateway },
onRetry = {
attemptedConnect = true
connectAttemptStartedAtMs = SystemClock.elapsedRealtime()
val config =
resolveGatewayConfig(
setupCode = setupCode,
@@ -496,6 +521,7 @@ private fun GatewayRecoveryScreen(
remoteAddress: String?,
ready: Boolean,
attemptedConnect: Boolean,
connectSettling: Boolean,
onAutoRetry: () -> Unit,
onBack: () -> Unit,
onRetry: () -> Unit,
@@ -503,9 +529,9 @@ private fun GatewayRecoveryScreen(
onContinue: () -> Unit,
modifier: Modifier = Modifier,
) {
val pairingRequired = gatewayStatusLooksLikePairing(statusText)
val recoveryState = gatewayRecoveryUiState(ready = ready, statusText = statusText, connectSettling = connectSettling)
val context = LocalContext.current
PairingAutoRetryEffect(enabled = pairingRequired && attemptedConnect && !ready, onRetry = onAutoRetry)
PairingAutoRetryEffect(enabled = recoveryState.canAutoRetry && attemptedConnect, onRetry = onAutoRetry)
ClawScaffold(modifier = modifier, contentPadding = PaddingValues(horizontal = 18.dp, vertical = 16.dp)) {
Column(modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.spacedBy(18.dp)) {
@@ -513,14 +539,26 @@ private fun GatewayRecoveryScreen(
Spacer(modifier = Modifier.height(12.dp))
Column(modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(12.dp)) {
Icon(
imageVector = if (ready) Icons.Default.CheckCircle else Icons.Default.ErrorOutline,
imageVector =
when (recoveryState) {
GatewayRecoveryUiState.Connected -> Icons.Default.CheckCircle
GatewayRecoveryUiState.Pairing -> Icons.Default.WifiTethering
GatewayRecoveryUiState.Finishing -> Icons.Default.WifiTethering
GatewayRecoveryUiState.Failed -> Icons.Default.ErrorOutline
},
contentDescription = null,
modifier = Modifier.size(64.dp),
tint = if (ready) ClawTheme.colors.success else ClawTheme.colors.warning,
tint =
when (recoveryState) {
GatewayRecoveryUiState.Connected -> ClawTheme.colors.success
GatewayRecoveryUiState.Pairing -> ClawTheme.colors.text
GatewayRecoveryUiState.Finishing -> ClawTheme.colors.text
GatewayRecoveryUiState.Failed -> ClawTheme.colors.warning
},
)
Text(text = if (ready) "Connected" else "Connection failed", style = ClawTheme.type.display, color = ClawTheme.colors.text)
Text(text = recoveryState.title, style = ClawTheme.type.display, color = ClawTheme.colors.text)
Text(
text = if (ready) "Your Gateway is ready." else "We could not reach your Gateway.\nLet's fix this.",
text = recoveryState.message,
style = ClawTheme.type.body,
color = ClawTheme.colors.textMuted,
textAlign = TextAlign.Center,
@@ -534,18 +572,30 @@ private fun GatewayRecoveryScreen(
Text(text = recoveryGatewayDetail(ready = ready, remoteAddress = remoteAddress, statusText = statusText), style = ClawTheme.type.body, color = ClawTheme.colors.textMuted)
ClawStatusPill(
text =
when {
ready -> "Healthy"
pairingRequired -> "Pairing"
else -> "Needs attention"
when (recoveryState) {
GatewayRecoveryUiState.Connected -> "Healthy"
GatewayRecoveryUiState.Pairing -> "Pairing"
GatewayRecoveryUiState.Finishing -> "Connecting"
GatewayRecoveryUiState.Failed -> "Needs attention"
},
status =
when (recoveryState) {
GatewayRecoveryUiState.Connected -> ClawStatus.Success
GatewayRecoveryUiState.Pairing -> ClawStatus.Neutral
GatewayRecoveryUiState.Finishing -> ClawStatus.Neutral
GatewayRecoveryUiState.Failed -> ClawStatus.Warning
},
status = if (ready) ClawStatus.Success else ClawStatus.Warning,
)
}
}
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
ClawPrimaryButton(text = if (ready) "Continue" else "Retry connection", icon = if (ready) Icons.Default.CheckCircle else Icons.Default.Refresh, onClick = if (ready) onContinue else onRetry, modifier = Modifier.fillMaxWidth())
ClawPrimaryButton(
text = if (ready) "Continue" else "Retry connection",
icon = if (ready) Icons.Default.CheckCircle else Icons.Default.Refresh,
onClick = if (ready) onContinue else onRetry,
modifier = Modifier.fillMaxWidth(),
)
OutlinedAction(title = "Edit connection", icon = Icons.Default.Edit, onClick = onEdit)
OutlinedAction(title = "Copy diagnostic", icon = Icons.Default.ContentCopy, onClick = { copyGatewayDiagnostic(context, statusText, serverName, remoteAddress, ready) })
}
@@ -562,7 +612,11 @@ private fun PermissionSetupScreen(
) {
ClawScaffold(modifier = modifier, contentPadding = PaddingValues(horizontal = 18.dp, vertical = 16.dp)) {
Column(modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.SpaceBetween) {
LazyColumn(contentPadding = PaddingValues(bottom = 14.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) {
LazyColumn(
modifier = Modifier.weight(1f),
contentPadding = PaddingValues(bottom = 14.dp),
verticalArrangement = Arrangement.spacedBy(6.dp),
) {
item {
PermissionTopBar(onBack = onBack)
}
@@ -824,6 +878,51 @@ private fun PermissionContinueButton(onClick: () -> Unit) {
}
}
internal enum class GatewayRecoveryUiState(
val title: String,
val message: String,
val canAutoRetry: Boolean,
) {
Connected(
title = "Connected",
message = "Your Gateway is ready.",
canAutoRetry = false,
),
Pairing(
title = "Pairing Gateway",
message = "Approval is in progress.\nOpenClaw will reconnect automatically.",
canAutoRetry = true,
),
Finishing(
title = "Finishing Setup",
message = "Gateway approved this phone.\nOpenClaw is bringing the node online.",
canAutoRetry = true,
),
Failed(
title = "Connection issue",
message = "We could not reach your Gateway.\nLet's fix this.",
canAutoRetry = false,
),
}
internal fun gatewayRecoveryUiState(
ready: Boolean,
statusText: String,
connectSettling: Boolean,
): GatewayRecoveryUiState =
when {
ready -> GatewayRecoveryUiState.Connected
connectSettling -> GatewayRecoveryUiState.Finishing
gatewayStatusLooksLikePairing(statusText) -> GatewayRecoveryUiState.Pairing
gatewayStatusLooksLikePartialConnect(statusText) -> GatewayRecoveryUiState.Finishing
else -> GatewayRecoveryUiState.Failed
}
internal fun gatewayStatusLooksLikePartialConnect(statusText: String): Boolean {
val lower = gatewayStatusForDisplay(statusText).lowercase()
return lower.contains("operator offline") || lower.contains("node offline")
}
private data class GatewayConfig(
val host: String,
val port: Int,

View File

@@ -70,6 +70,7 @@ import androidx.compose.material.icons.filled.Notifications
import androidx.compose.material.icons.filled.Palette
import androidx.compose.material.icons.filled.Person
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.filled.QrCode2
import androidx.compose.material.icons.filled.Storage
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
@@ -90,6 +91,7 @@ import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.content.ContextCompat
internal enum class SettingsRoute {
@@ -676,13 +678,20 @@ private fun GatewaySettingsScreen(
SettingsMetric("Node", if (isNodeConnected) "Online" else "Not paired"),
SettingsMetric("Gateway", serverName?.takeIf { it.isNotBlank() } ?: "Home Gateway"),
SettingsMetric("Address", remoteAddress?.takeIf { it.isNotBlank() } ?: "Not available"),
SettingsMetric("Status", statusText),
SettingsMetric("Status", gatewayStatusLabel(statusText = statusText, isConnected = isConnected)),
),
)
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
ClawPrimaryButton(text = "Reconnect", onClick = viewModel::refreshGatewayConnection, modifier = Modifier.weight(1f))
ClawSecondaryButton(text = "Disconnect", onClick = viewModel::disconnect, modifier = Modifier.weight(1f))
}
ClawPanel {
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
Text(text = "Pair New Gateway", style = ClawTheme.type.section, color = ClawTheme.colors.text)
Text(text = "Clear this phone's saved gateway access and scan a fresh setup code.", style = ClawTheme.type.body, color = ClawTheme.colors.textMuted)
ClawSecondaryButton(text = "Pair New Gateway", onClick = viewModel::pairNewGateway, modifier = Modifier.fillMaxWidth(), icon = Icons.Default.QrCode2)
}
}
ClawPanel {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text(text = "Connection Setup", style = ClawTheme.type.section, color = ClawTheme.colors.text)
@@ -775,6 +784,23 @@ private fun AppearanceSettingsScreen(onBack: () -> Unit) {
}
}
private fun gatewayStatusLabel(
statusText: String,
isConnected: Boolean,
): String {
if (isConnected) return "Ready"
val status = statusText.trim().lowercase()
return when {
status.contains("connecting") || status.contains("reconnecting") -> "Connecting..."
status.contains("pair") -> "Pairing needed"
status.contains("auth") -> "Authentication needed"
status.contains("certificate") || status.contains("tls") -> "Certificate review needed"
status.contains("failed") || status.contains("error") || status.contains("offline") || status.contains("not connected") -> "Cannot reach gateway"
status.isBlank() -> "Not connected"
else -> "Not connected"
}
}
@Composable
private fun AboutSettingsScreen(
viewModel: MainViewModel,
@@ -850,7 +876,7 @@ internal fun SettingsDetailFrame(
onBack: () -> Unit,
content: @Composable () -> Unit,
) {
ClawScaffold(contentPadding = PaddingValues(start = 20.dp, top = 14.dp, end = 20.dp, bottom = 20.dp)) {
ClawScaffold(contentPadding = PaddingValues(start = ClawTheme.spacing.lg, top = 14.dp, end = ClawTheme.spacing.lg, bottom = 20.dp)) {
LazyColumn(verticalArrangement = Arrangement.spacedBy(10.dp)) {
item {
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(9.dp)) {
@@ -863,7 +889,9 @@ internal fun SettingsDetailFrame(
Text(text = subtitle, style = ClawTheme.type.body, color = ClawTheme.colors.textMuted)
}
item {
content()
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
content()
}
}
item {
Spacer(modifier = Modifier.height(12.dp))
@@ -1097,11 +1125,11 @@ private fun SettingsToggleListRow(row: SettingsToggleRow) {
@Composable
internal fun SettingsMetricPanel(rows: List<SettingsMetric>) {
ClawPanel(contentPadding = PaddingValues(horizontal = 0.dp, vertical = 0.dp)) {
ClawPanel(contentPadding = PaddingValues(horizontal = 14.dp, vertical = 4.dp)) {
ClawSeparatedColumn(items = rows) { row ->
Row(modifier = Modifier.fillMaxWidth().padding(horizontal = 10.dp, vertical = 6.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Row(modifier = Modifier.fillMaxWidth().heightIn(min = 50.dp).padding(horizontal = 0.dp, vertical = 7.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Text(text = row.title, style = ClawTheme.type.body, color = ClawTheme.colors.text, modifier = Modifier.weight(1f), maxLines = 1)
Text(text = row.value, style = ClawTheme.type.caption, color = ClawTheme.colors.textMuted, maxLines = 1, overflow = TextOverflow.Ellipsis)
Text(text = row.value, style = ClawTheme.type.caption.copy(fontSize = 13.sp, lineHeight = 17.sp), color = ClawTheme.colors.textMuted, maxLines = 1, overflow = TextOverflow.Ellipsis)
}
}
}

View File

@@ -151,6 +151,11 @@ fun ShellScreen(
VoiceShellScreen(
viewModel = viewModel,
onOpenCommand = { commandOpen = true },
onOpenGatewaySettings = {
settingsRoute = SettingsRoute.Gateway
returnToOverviewFromSettings = false
activeTab = Tab.Settings
},
onOpenVoiceSettings = {
settingsRoute = SettingsRoute.Voice
returnToOverviewFromSettings = false
@@ -304,7 +309,7 @@ private fun OverviewScreen(
ClawScaffold(contentPadding = PaddingValues(start = 20.dp, top = 14.dp, end = 20.dp, bottom = 20.dp)) {
Box(modifier = Modifier.fillMaxSize()) {
LazyColumn(verticalArrangement = Arrangement.spacedBy(10.dp), contentPadding = PaddingValues(bottom = 82.dp)) {
LazyColumn(verticalArrangement = Arrangement.spacedBy(10.dp), contentPadding = PaddingValues(bottom = 104.dp)) {
item {
Row(
modifier = Modifier.fillMaxWidth(),
@@ -402,7 +407,7 @@ private fun OverviewScreen(
}
}
}
OverviewChatButton(onClick = { onSelectTab(Tab.Chat) }, modifier = Modifier.align(Alignment.BottomEnd).padding(bottom = 8.dp))
OverviewChatButton(onClick = { onSelectTab(Tab.Chat) }, modifier = Modifier.align(Alignment.BottomEnd).padding(bottom = 20.dp))
}
}
}
@@ -424,17 +429,17 @@ private fun OverviewChatButton(
Surface(
onClick = onClick,
modifier = modifier.height(ClawTheme.spacing.touchTarget),
shape = RoundedCornerShape(ClawTheme.radii.pill),
shape = RoundedCornerShape(ClawTheme.radii.button),
color = ClawTheme.colors.primary,
contentColor = ClawTheme.colors.primaryText,
) {
Row(
modifier = Modifier.padding(horizontal = 18.dp),
modifier = Modifier.padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(7.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
Icon(imageVector = Icons.Outlined.ChatBubbleOutline, contentDescription = null, modifier = Modifier.size(18.dp))
Text(text = "Chat", style = ClawTheme.type.title.copy(fontSize = 17.sp, lineHeight = 22.sp))
Text(text = "Chat", style = ClawTheme.type.label.copy(fontSize = 16.sp, lineHeight = 20.sp))
}
}
}
@@ -475,7 +480,7 @@ private fun ModuleList(
onSelectTab: (Tab) -> Unit,
onOpenSettingsRoute: (SettingsRoute) -> Unit,
) {
ClawPanel(contentPadding = PaddingValues(horizontal = 8.dp, vertical = 0.dp)) {
ClawPanel(contentPadding = PaddingValues(horizontal = 14.dp, vertical = 4.dp)) {
Column(verticalArrangement = Arrangement.spacedBy(0.dp)) {
rows.forEachIndexed { index, row ->
ModuleListRow(
@@ -490,7 +495,7 @@ private fun ModuleList(
},
)
if (index != rows.lastIndex) {
HorizontalDivider(color = ClawTheme.colors.border, thickness = 1.dp)
HorizontalDivider(color = ClawTheme.colors.border.copy(alpha = 0.82f), thickness = 1.dp)
}
}
}
@@ -507,14 +512,14 @@ private fun ModuleListRow(
modifier =
Modifier
.fillMaxWidth()
.heightIn(min = 50.dp)
.heightIn(min = 54.dp)
.clip(RoundedCornerShape(ClawTheme.radii.row))
.clickable(onClick = onClick)
.padding(horizontal = 2.dp, vertical = 5.dp),
.padding(horizontal = 0.dp, vertical = 6.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(9.dp),
) {
Icon(imageVector = row.icon, contentDescription = null, modifier = Modifier.size(19.dp), tint = ClawTheme.colors.text)
Icon(imageVector = row.icon, contentDescription = null, modifier = Modifier.size(20.dp), tint = ClawTheme.colors.text)
Text(
text = row.title,
style = ClawTheme.type.body,
@@ -526,7 +531,7 @@ private fun ModuleListRow(
row.metadata?.let {
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp)) {
Box(modifier = Modifier.size(4.5.dp).clip(CircleShape).background(statusDotColor(it)))
Text(text = it, style = ClawTheme.type.caption.copy(fontSize = 12.5.sp, lineHeight = 16.sp), color = ClawTheme.colors.textMuted, maxLines = 1)
Text(text = it, style = ClawTheme.type.caption.copy(fontSize = 13.sp, lineHeight = 17.sp), color = ClawTheme.colors.textMuted, maxLines = 1, overflow = TextOverflow.Ellipsis)
}
}
Icon(
@@ -561,7 +566,7 @@ private fun RecentSessionList(
rows: List<RecentSessionListItem>,
onOpen: (String) -> Unit,
) {
ClawPanel(contentPadding = PaddingValues(horizontal = 8.dp, vertical = 0.dp)) {
ClawPanel(contentPadding = PaddingValues(horizontal = 14.dp, vertical = 4.dp)) {
Column {
rows.forEachIndexed { index, row ->
RecentSessionRowContent(
@@ -571,7 +576,7 @@ private fun RecentSessionList(
onClick = { onOpen(row.key) },
)
if (index != rows.lastIndex) {
HorizontalDivider(color = ClawTheme.colors.border, thickness = 1.dp)
HorizontalDivider(color = ClawTheme.colors.border.copy(alpha = 0.82f), thickness = 1.dp)
}
}
}
@@ -593,7 +598,7 @@ private fun RecentSessionRowContent(
.heightIn(min = 58.dp)
.clip(RoundedCornerShape(ClawTheme.radii.row))
.clickable(onClick = onClick)
.padding(horizontal = 2.dp, vertical = 6.dp),
.padding(horizontal = 0.dp, vertical = 7.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
@@ -609,9 +614,9 @@ private fun RecentSessionRowContent(
}
Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(1.dp)) {
Text(text = title, style = ClawTheme.type.body, color = ClawTheme.colors.text, maxLines = 1)
Text(text = subtitle, style = ClawTheme.type.caption.copy(fontSize = 12.5.sp, lineHeight = 16.sp), color = ClawTheme.colors.textSubtle, maxLines = 1)
Text(text = subtitle, style = ClawTheme.type.caption.copy(fontSize = 13.sp, lineHeight = 17.sp), color = ClawTheme.colors.textSubtle, maxLines = 1)
}
Text(text = metadata, style = ClawTheme.type.caption.copy(fontSize = 12.5.sp, lineHeight = 16.sp), color = ClawTheme.colors.textMuted)
Text(text = metadata, style = ClawTheme.type.caption.copy(fontSize = 13.sp, lineHeight = 17.sp), color = ClawTheme.colors.textMuted)
Icon(
imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight,
contentDescription = "Open session",
@@ -637,10 +642,16 @@ private fun ChatShellScreen(
private fun VoiceShellScreen(
viewModel: MainViewModel,
onOpenCommand: () -> Unit,
onOpenGatewaySettings: () -> Unit,
onOpenVoiceSettings: () -> Unit,
) {
ClawScaffold(contentPadding = PaddingValues(start = 0.dp, top = 8.dp, end = 0.dp, bottom = 8.dp)) {
VoiceScreen(viewModel = viewModel, onOpenCommand = onOpenCommand, onOpenVoiceSettings = onOpenVoiceSettings)
VoiceScreen(
viewModel = viewModel,
onOpenCommand = onOpenCommand,
onOpenGatewaySettings = onOpenGatewaySettings,
onOpenVoiceSettings = onOpenVoiceSettings,
)
}
}
@@ -903,7 +914,7 @@ private fun SettingsGroup(
onOpen: (SettingsRoute) -> Unit,
onAction: (() -> Unit)? = null,
) {
ClawPanel(contentPadding = PaddingValues(horizontal = 0.dp, vertical = 0.dp)) {
ClawPanel(contentPadding = PaddingValues(horizontal = 12.dp, vertical = 4.dp)) {
Column {
rows.forEachIndexed { index, row ->
SettingsListRow(
@@ -918,7 +929,7 @@ private fun SettingsGroup(
},
)
if (index != rows.lastIndex) {
HorizontalDivider(color = ClawTheme.colors.border, thickness = 1.dp)
HorizontalDivider(color = ClawTheme.colors.border.copy(alpha = 0.82f), thickness = 1.dp)
}
}
}
@@ -934,17 +945,17 @@ private fun SettingsListRow(
modifier =
Modifier
.fillMaxWidth()
.heightIn(min = 52.dp)
.heightIn(min = 54.dp)
.clip(RoundedCornerShape(ClawTheme.radii.row))
.clickable(onClick = onClick)
.padding(horizontal = 10.dp, vertical = 6.dp),
.padding(horizontal = 0.dp, vertical = 7.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(10.dp),
) {
Icon(imageVector = row.icon, contentDescription = null, modifier = Modifier.size(19.dp), tint = ClawTheme.colors.text)
Icon(imageVector = row.icon, contentDescription = null, modifier = Modifier.size(20.dp), tint = ClawTheme.colors.text)
Text(text = row.title, style = ClawTheme.type.body, color = ClawTheme.colors.text, modifier = Modifier.weight(1f), maxLines = 1)
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(5.dp)) {
Text(text = row.value, style = ClawTheme.type.caption.copy(fontSize = 12.5.sp, lineHeight = 16.sp), color = ClawTheme.colors.textMuted, maxLines = 1)
Text(text = row.value, style = ClawTheme.type.caption.copy(fontSize = 13.sp, lineHeight = 17.sp), color = ClawTheme.colors.textMuted, maxLines = 1, overflow = TextOverflow.Ellipsis)
row.status?.let { active ->
Box(modifier = Modifier.size(4.5.dp).clip(CircleShape).background(if (active) ClawTheme.colors.success else ClawTheme.colors.textSubtle))
}

View File

@@ -42,6 +42,7 @@ import androidx.compose.material.icons.automirrored.filled.Send
import androidx.compose.material.icons.automirrored.filled.VolumeOff
import androidx.compose.material.icons.automirrored.filled.VolumeUp
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Cloud
import androidx.compose.material.icons.filled.GraphicEq
import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.Mic
@@ -78,6 +79,7 @@ import androidx.core.content.ContextCompat
fun VoiceScreen(
viewModel: MainViewModel,
onOpenCommand: () -> Unit,
onOpenGatewaySettings: () -> Unit,
onOpenVoiceSettings: () -> Unit,
) {
val context = LocalContext.current
@@ -113,6 +115,7 @@ fun VoiceScreen(
val activeConversation = if (voiceCaptureMode == VoiceCaptureMode.TalkMode) talkModeConversation else micConversation
val voiceActive = micEnabled || micIsSending || talkModeEnabled
val gatewayReady = gatewayStatus.isVoiceGatewayReady()
val activeStatus =
voiceStatusLabel(
gatewayStatus = gatewayStatus,
@@ -158,10 +161,10 @@ fun VoiceScreen(
.fillMaxSize()
.imePadding()
.padding(horizontal = 20.dp, vertical = 8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(10.dp),
) {
VoiceHeader(
statusText = if (voiceActive) activeStatus else "Your voice command center.",
statusText = if (voiceActive || !gatewayReady) activeStatus else "Your voice command center.",
speakerEnabled = speakerEnabled,
onToggleSpeaker = { viewModel.setSpeakerEnabled(!speakerEnabled) },
onOpenCommand = onOpenCommand,
@@ -175,6 +178,7 @@ fun VoiceScreen(
talkModeListening = talkModeListening,
talkModeSpeaking = talkModeSpeaking,
micLiveTranscript = micLiveTranscript,
gatewayReady = gatewayReady,
onStartTalk = {
runVoiceAction(
action = VoiceAction.Talk,
@@ -198,6 +202,7 @@ fun VoiceScreen(
run = { viewModel.setMicEnabled(!micEnabled) },
)
},
onConnectGateway = onOpenGatewaySettings,
)
if (!hasMicPermission) {
@@ -366,12 +371,12 @@ private fun TalkSessionScreen(
.fillMaxSize()
.imePadding()
.padding(horizontal = 20.dp, vertical = 8.dp),
verticalArrangement = Arrangement.spacedBy(11.dp),
verticalArrangement = Arrangement.spacedBy(10.dp),
) {
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
VoicePlainIconButton(icon = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back to voice", onClick = onEndTalk)
Column(modifier = Modifier.weight(1f), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(3.dp)) {
Text(text = "Realtime Talk", style = ClawTheme.type.title.copy(fontSize = 14.sp, lineHeight = 17.sp), color = ClawTheme.colors.text)
Text(text = "Realtime Talk", style = ClawTheme.type.title.copy(fontSize = 16.sp, lineHeight = 20.sp), color = ClawTheme.colors.text)
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(5.dp)) {
Box(modifier = Modifier.size(4.5.dp).clip(CircleShape).background(if (speaking || listening) ClawTheme.colors.success else ClawTheme.colors.textSubtle))
Text(
@@ -392,8 +397,8 @@ private fun TalkSessionScreen(
}
Surface(
modifier = Modifier.fillMaxWidth().height(58.dp),
shape = RoundedCornerShape(ClawTheme.radii.pill),
modifier = Modifier.fillMaxWidth().height(52.dp),
shape = RoundedCornerShape(ClawTheme.radii.panel),
color = ClawTheme.colors.canvas,
border = BorderStroke(1.dp, ClawTheme.colors.borderStrong),
) {
@@ -424,7 +429,7 @@ private fun TalkTranscript(
entries: List<VoiceConversationEntry>,
modifier: Modifier = Modifier,
) {
LazyColumn(modifier = modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(7.dp)) {
LazyColumn(modifier = modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(8.dp)) {
if (entries.isEmpty()) {
item {
TalkTranscriptCard(label = "OpenClaw", text = "Listening for your next turn.", muted = true)
@@ -453,7 +458,7 @@ private fun TalkTranscriptCard(
color = ClawTheme.colors.surface,
border = BorderStroke(1.dp, ClawTheme.colors.border),
) {
Column(modifier = Modifier.padding(horizontal = 10.dp, vertical = 8.dp), verticalArrangement = Arrangement.spacedBy(5.dp)) {
Column(modifier = Modifier.padding(horizontal = 14.dp, vertical = 9.dp), verticalArrangement = Arrangement.spacedBy(5.dp)) {
Text(text = label, style = ClawTheme.type.section, color = ClawTheme.colors.text)
Text(text = text, style = ClawTheme.type.body, color = if (muted) ClawTheme.colors.textMuted else ClawTheme.colors.text)
}
@@ -471,7 +476,7 @@ private fun TalkControl(
Surface(
onClick = onClick,
modifier = Modifier.size(ClawTheme.spacing.touchTarget),
shape = CircleShape,
shape = RoundedCornerShape(ClawTheme.radii.button),
color = if (primary) ClawTheme.colors.primary else ClawTheme.colors.canvas,
contentColor = if (primary) ClawTheme.colors.primaryText else ClawTheme.colors.text,
border = BorderStroke(1.dp, if (primary) ClawTheme.colors.primary else ClawTheme.colors.border),
@@ -582,10 +587,12 @@ private fun VoiceHero(
talkModeListening: Boolean,
talkModeSpeaking: Boolean,
micLiveTranscript: String?,
gatewayReady: Boolean,
onStartTalk: () -> Unit,
onStartDictation: () -> Unit,
onConnectGateway: () -> Unit,
) {
Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(10.dp)) {
Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(9.dp)) {
VoiceOrb(
active = micEnabled || talkModeEnabled,
listening = talkModeListening || voiceCaptureMode == VoiceCaptureMode.ManualMic,
@@ -607,6 +614,7 @@ private fun VoiceHero(
talkModeListening -> "Listening"
talkModeEnabled -> "Talk is live"
micEnabled -> "Dictation is listening"
!gatewayReady -> "Gateway offline"
else -> "Ready to talk"
},
style = ClawTheme.type.body,
@@ -631,27 +639,49 @@ private fun VoiceHero(
}
}
ClawPanel(contentPadding = PaddingValues(horizontal = 0.dp, vertical = 0.dp)) {
ClawPanel(contentPadding = PaddingValues(horizontal = 14.dp, vertical = 4.dp)) {
VoiceModeRow(
title = if (talkModeEnabled) "End Talk" else "Realtime Talk",
subtitle = if (talkModeEnabled) "Conversation is live" else "Natural conversation in real time",
subtitle =
when {
talkModeEnabled -> "Conversation is live"
gatewayReady -> "Natural conversation in real time"
else -> "Connect gateway to start"
},
icon = if (talkModeEnabled) Icons.Default.PhoneDisabled else Icons.Default.RecordVoiceOver,
onClick = onStartTalk,
enabled = gatewayReady || talkModeEnabled,
)
VoiceModeRow(
title = if (micEnabled) "Stop Dictation" else "Dictation",
subtitle = if (micEnabled) "Listening for one turn" else "Convert speech to text",
subtitle =
when {
micEnabled -> "Listening for one turn"
gatewayReady -> "Convert speech to text"
else -> "Connect gateway to start"
},
icon = if (micEnabled) Icons.Default.MicOff else Icons.Default.TextFields,
onClick = onStartDictation,
enabled = gatewayReady || micEnabled,
)
}
VoiceProviderCard(gatewayStatus = gatewayStatus)
VoicePrimaryAction(
text = if (talkModeEnabled) "End Talk" else "Start Talk",
icon = if (talkModeEnabled) Icons.Default.PhoneDisabled else Icons.Default.Phone,
onClick = onStartTalk,
text =
when {
talkModeEnabled -> "End Talk"
gatewayReady -> "Start Talk"
else -> "Connect Gateway"
},
icon =
when {
talkModeEnabled -> Icons.Default.PhoneDisabled
gatewayReady -> Icons.Default.Phone
else -> Icons.Default.Cloud
},
onClick = if (gatewayReady || talkModeEnabled) onStartTalk else onConnectGateway,
)
}
}
@@ -662,29 +692,37 @@ private fun VoiceModeRow(
subtitle: String,
icon: androidx.compose.ui.graphics.vector.ImageVector,
onClick: () -> Unit,
enabled: Boolean = true,
) {
Surface(onClick = onClick, color = Color.Transparent, contentColor = ClawTheme.colors.text) {
Surface(onClick = onClick, enabled = enabled, color = Color.Transparent, contentColor = ClawTheme.colors.text) {
Row(
modifier = Modifier.fillMaxWidth().heightIn(min = 60.dp).padding(horizontal = 10.dp, vertical = 6.dp),
modifier = Modifier.fillMaxWidth().heightIn(min = 54.dp).padding(horizontal = 0.dp, vertical = 7.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(10.dp),
) {
Surface(
modifier = Modifier.size(34.dp),
shape = CircleShape,
color = ClawTheme.colors.surface,
contentColor = ClawTheme.colors.text,
modifier = Modifier.size(30.dp),
shape = RoundedCornerShape(ClawTheme.radii.control),
color = if (enabled) ClawTheme.colors.surface else ClawTheme.colors.canvas,
contentColor = if (enabled) ClawTheme.colors.text else ClawTheme.colors.textSubtle,
border = BorderStroke(1.dp, ClawTheme.colors.border),
) {
Box(contentAlignment = Alignment.Center) {
Icon(imageVector = icon, contentDescription = null, modifier = Modifier.size(16.dp))
Icon(imageVector = icon, contentDescription = null, modifier = Modifier.size(15.dp))
}
}
Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp)) {
Text(text = title, style = ClawTheme.type.body, color = ClawTheme.colors.text, maxLines = 1)
Text(text = title, style = ClawTheme.type.body, color = if (enabled) ClawTheme.colors.text else ClawTheme.colors.textMuted, maxLines = 1)
Text(text = subtitle, style = ClawTheme.type.caption.copy(fontSize = 12.5.sp, lineHeight = 16.sp), color = ClawTheme.colors.textMuted, maxLines = 1)
}
Icon(imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, contentDescription = null, modifier = Modifier.size(21.dp), tint = ClawTheme.colors.textMuted)
if (enabled) {
Icon(
imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight,
contentDescription = null,
modifier = Modifier.size(18.dp),
tint = ClawTheme.colors.textMuted,
)
}
}
}
}
@@ -700,19 +738,19 @@ private fun VoiceProviderCard(gatewayStatus: String) {
border = BorderStroke(1.dp, ClawTheme.colors.border),
) {
Row(
modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp, vertical = 9.dp),
modifier = Modifier.fillMaxWidth().padding(horizontal = 14.dp, vertical = 9.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(10.dp),
) {
Surface(
modifier = Modifier.size(34.dp),
shape = CircleShape,
modifier = Modifier.size(30.dp),
shape = RoundedCornerShape(ClawTheme.radii.control),
color = ClawTheme.colors.canvas,
contentColor = ClawTheme.colors.text,
border = BorderStroke(1.dp, ClawTheme.colors.borderStrong),
) {
Box(contentAlignment = Alignment.Center) {
Icon(imageVector = Icons.Default.GraphicEq, contentDescription = null, modifier = Modifier.size(17.dp))
Icon(imageVector = Icons.Default.GraphicEq, contentDescription = null, modifier = Modifier.size(15.dp))
}
}
Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp)) {
@@ -742,7 +780,7 @@ private fun VoicePrimaryAction(
Surface(
onClick = onClick,
modifier = Modifier.fillMaxWidth().height(ClawTheme.spacing.touchTarget),
shape = RoundedCornerShape(ClawTheme.radii.pill),
shape = RoundedCornerShape(ClawTheme.radii.button),
color = ClawTheme.colors.primary,
contentColor = ClawTheme.colors.primaryText,
) {
@@ -764,7 +802,7 @@ private fun VoiceOrb(
speaking: Boolean,
) {
Surface(
modifier = Modifier.size(132.dp),
modifier = Modifier.size(112.dp),
shape = CircleShape,
color = if (active) ClawTheme.colors.surfacePressed else ClawTheme.colors.surface,
border = BorderStroke(1.dp, if (active) ClawTheme.colors.borderStrong else ClawTheme.colors.border),
@@ -779,7 +817,7 @@ private fun VoiceOrb(
else -> Icons.Default.Mic
},
contentDescription = null,
modifier = Modifier.size(38.dp),
modifier = Modifier.size(32.dp),
tint = ClawTheme.colors.text,
)
Waveform(active = active)
@@ -837,7 +875,7 @@ private fun VoiceTranscript(
item {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text(text = "Live transcript", style = ClawTheme.type.caption, color = ClawTheme.colors.textSubtle)
ClawPanel(contentPadding = PaddingValues(horizontal = 10.dp, vertical = 9.dp)) {
ClawPanel(contentPadding = PaddingValues(horizontal = 14.dp, vertical = 9.dp)) {
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
Text(text = "No transcript yet", style = ClawTheme.type.section, color = ClawTheme.colors.text)
Text(
@@ -864,7 +902,7 @@ private fun VoiceTurnCard(entry: VoiceConversationEntry) {
contentColor = ClawTheme.colors.text,
border = BorderStroke(1.dp, if (entry.isStreaming) ClawTheme.colors.borderStrong else ClawTheme.colors.border),
) {
Column(modifier = Modifier.padding(horizontal = 10.dp, vertical = 8.dp), verticalArrangement = Arrangement.spacedBy(5.dp)) {
Column(modifier = Modifier.padding(horizontal = 14.dp, vertical = 9.dp), verticalArrangement = Arrangement.spacedBy(5.dp)) {
Text(
text = if (isUser) "You" else "OpenClaw",
style = ClawTheme.type.caption.copy(fontSize = 12.5.sp, lineHeight = 16.sp, fontWeight = FontWeight.SemiBold),

View File

@@ -10,6 +10,7 @@ import ai.openclaw.app.ui.mobileText
import ai.openclaw.app.ui.mobileTextSecondary
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
@@ -17,6 +18,7 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -29,6 +31,7 @@ import androidx.compose.ui.unit.dp
@Composable
fun ChatMessageListCard(
messages: List<ChatMessage>,
historyLoading: Boolean,
pendingRunCount: Int,
pendingToolCalls: List<ChatPendingToolCall>,
streamingAssistantText: String?,
@@ -86,7 +89,30 @@ fun ChatMessageListCard(
}
if (messages.isEmpty() && pendingRunCount == 0 && pendingToolCalls.isEmpty() && streamingAssistantText.isNullOrBlank()) {
EmptyChatHint(modifier = Modifier.align(Alignment.Center), healthOk = healthOk)
if (historyLoading) {
LoadingChatHint(modifier = Modifier.align(Alignment.Center))
} else {
EmptyChatHint(modifier = Modifier.align(Alignment.Center), healthOk = healthOk)
}
}
}
}
@Composable
private fun LoadingChatHint(modifier: Modifier = Modifier) {
Surface(
modifier = modifier.fillMaxWidth(),
shape = RoundedCornerShape(14.dp),
color = mobileCardSurface.copy(alpha = 0.9f),
border = androidx.compose.foundation.BorderStroke(1.dp, mobileBorder),
) {
Column(
modifier = Modifier.padding(horizontal = 12.dp, vertical = 12.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
CircularProgressIndicator(color = mobileText, strokeWidth = 2.dp)
Text("Loading session", style = mobileCallout, color = mobileTextSecondary)
}
}
}

View File

@@ -62,7 +62,8 @@ fun ChatMessageBubble(message: ChatMessage) {
message.content.filter { part ->
when (part.type) {
"text" -> !part.text.isNullOrBlank()
else -> part.base64 != null
"image" -> !part.base64.isNullOrBlank()
else -> false
}
}

View File

@@ -6,6 +6,7 @@ import ai.openclaw.app.chat.ChatMessageContent
import ai.openclaw.app.chat.ChatPendingToolCall
import ai.openclaw.app.chat.OutgoingAttachment
import ai.openclaw.app.ui.design.ClawListItem
import ai.openclaw.app.ui.design.ClawLoadingState
import ai.openclaw.app.ui.design.ClawPanel
import ai.openclaw.app.ui.design.ClawStatus
import ai.openclaw.app.ui.design.ClawStatusPill
@@ -80,6 +81,7 @@ fun ChatScreen(
onVoice: () -> Unit,
) {
val messages by viewModel.chatMessages.collectAsState()
val historyLoading by viewModel.chatHistoryLoading.collectAsState()
val errorText by viewModel.chatError.collectAsState()
val pendingRunCount by viewModel.pendingRunCount.collectAsState()
val healthOk by viewModel.chatHealthOk.collectAsState()
@@ -168,6 +170,7 @@ fun ChatScreen(
ChatMessageList(
messages = messages,
historyLoading = historyLoading,
pendingRunCount = pendingRunCount,
pendingToolCalls = pendingToolCalls,
streamingAssistantText = streamingAssistantText,
@@ -307,6 +310,7 @@ private fun HeaderIcon(
@Composable
private fun ChatMessageList(
messages: List<ChatMessage>,
historyLoading: Boolean,
pendingRunCount: Int,
pendingToolCalls: List<ChatPendingToolCall>,
streamingAssistantText: String?,
@@ -359,7 +363,11 @@ private fun ChatMessageList(
}
if (messages.isEmpty() && pendingRunCount == 0 && pendingToolCalls.isEmpty() && stream.isNullOrBlank()) {
EmptyChatHint(healthOk = healthOk, onStarterPrompt = onStarterPrompt, modifier = Modifier.align(Alignment.Center))
if (historyLoading) {
ClawLoadingState(title = "Loading session", modifier = Modifier.align(Alignment.Center))
} else {
EmptyChatHint(healthOk = healthOk, onStarterPrompt = onStarterPrompt, modifier = Modifier.align(Alignment.Center))
}
}
}
}
@@ -464,7 +472,8 @@ private fun ChatBubble(
content.filter { part ->
when (part.type) {
"text" -> !part.text.isNullOrBlank()
else -> part.base64 != null
"image" -> !part.base64.isNullOrBlank()
else -> false
}
}
if (displayableContent.isEmpty()) return

View File

@@ -84,6 +84,7 @@ internal fun resolveInitialChatLoadSessionKey(
@Composable
fun ChatSheetContent(viewModel: MainViewModel) {
val messages by viewModel.chatMessages.collectAsState()
val historyLoading by viewModel.chatHistoryLoading.collectAsState()
val errorText by viewModel.chatError.collectAsState()
val pendingRunCount by viewModel.pendingRunCount.collectAsState()
val healthOk by viewModel.chatHealthOk.collectAsState()
@@ -164,6 +165,7 @@ fun ChatSheetContent(viewModel: MainViewModel) {
ChatMessageListCard(
messages = messages,
historyLoading = historyLoading,
pendingRunCount = pendingRunCount,
pendingToolCalls = pendingToolCalls,
streamingAssistantText = streamingAssistantText,

View File

@@ -47,6 +47,7 @@ import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
internal enum class ClawStatus {
Neutral,
@@ -105,7 +106,7 @@ internal fun ClawPrimaryButton(
onClick = onClick,
enabled = enabled,
modifier = modifier.heightIn(min = ClawTheme.spacing.touchTarget),
shape = RoundedCornerShape(ClawTheme.radii.pill),
shape = RoundedCornerShape(ClawTheme.radii.button),
colors =
ButtonDefaults.buttonColors(
containerColor = ClawTheme.colors.primary,
@@ -136,7 +137,7 @@ internal fun ClawSecondaryButton(
onClick = onClick,
enabled = enabled,
modifier = modifier.heightIn(min = ClawTheme.spacing.touchTarget),
shape = RoundedCornerShape(ClawTheme.radii.pill),
shape = RoundedCornerShape(ClawTheme.radii.button),
color = if (enabled) ClawTheme.colors.surfaceRaised else ClawTheme.colors.surface,
contentColor = if (enabled) ClawTheme.colors.text else ClawTheme.colors.textSubtle,
border = BorderStroke(1.dp, if (enabled) ClawTheme.colors.borderStrong else ClawTheme.colors.border),
@@ -195,7 +196,7 @@ internal fun ClawStatusPill(
Surface(
modifier = modifier,
shape = RoundedCornerShape(ClawTheme.radii.pill),
shape = RoundedCornerShape(ClawTheme.radii.control),
color = backgroundColor,
border = BorderStroke(1.dp, ClawTheme.colors.border),
) {
@@ -207,11 +208,11 @@ internal fun ClawStatusPill(
Box(
modifier =
Modifier
.size(6.dp)
.size(5.dp)
.clip(CircleShape)
.background(dotColor),
)
Text(text = text, style = ClawTheme.type.caption, color = ClawTheme.colors.textMuted, maxLines = 1)
Text(text = text, style = ClawTheme.type.caption.copy(fontSize = 13.sp, lineHeight = 17.sp), color = ClawTheme.colors.textMuted, maxLines = 1)
}
}
}
@@ -253,7 +254,7 @@ internal fun <T> ClawListPanel(
modifier: Modifier = Modifier,
row: @Composable (T) -> Unit,
) {
ClawPanel(modifier = modifier, contentPadding = PaddingValues(horizontal = 0.dp, vertical = 0.dp)) {
ClawPanel(modifier = modifier, contentPadding = PaddingValues(horizontal = 14.dp, vertical = 4.dp)) {
ClawSeparatedColumn(items = items, row = row)
}
}
@@ -268,7 +269,7 @@ internal fun <T> ClawSeparatedColumn(
items.forEachIndexed { index, item ->
row(item)
if (index != items.lastIndex) {
HorizontalDivider(color = ClawTheme.colors.border, thickness = 1.dp)
HorizontalDivider(color = ClawTheme.colors.border.copy(alpha = 0.82f), thickness = 1.dp)
}
}
}
@@ -286,8 +287,8 @@ internal fun ClawDetailRow(
modifier =
modifier
.fillMaxWidth()
.heightIn(min = 52.dp)
.padding(horizontal = 12.dp, vertical = 5.dp),
.heightIn(min = 54.dp)
.padding(horizontal = 0.dp, vertical = 7.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(9.dp),
) {
@@ -399,8 +400,8 @@ internal fun ClawSegmentedControl(
Row(
modifier =
modifier
.clip(RoundedCornerShape(ClawTheme.radii.pill))
.border(1.dp, ClawTheme.colors.border, RoundedCornerShape(ClawTheme.radii.pill))
.clip(RoundedCornerShape(ClawTheme.radii.control))
.border(1.dp, ClawTheme.colors.border, RoundedCornerShape(ClawTheme.radii.control))
.padding(2.dp),
horizontalArrangement = Arrangement.spacedBy(2.dp),
) {
@@ -410,7 +411,7 @@ internal fun ClawSegmentedControl(
modifier =
Modifier
.weight(1f)
.clip(RoundedCornerShape(ClawTheme.radii.pill))
.clip(RoundedCornerShape(ClawTheme.radii.control))
.background(if (active) ClawTheme.colors.primary else Color.Transparent)
.clickable { onSelect(option) }
.padding(horizontal = 9.dp, vertical = 7.dp),

View File

@@ -18,7 +18,7 @@ import androidx.compose.ui.unit.dp
@Composable
internal fun ClawPanel(
modifier: Modifier = Modifier,
contentPadding: PaddingValues = PaddingValues(9.dp),
contentPadding: PaddingValues = PaddingValues(12.dp),
content: @Composable () -> Unit,
) {
Surface(

View File

@@ -57,10 +57,11 @@ internal data class ClawSpacing(
@Immutable
internal data class ClawRadii(
val row: Dp = 4.dp,
val panel: Dp = 7.dp,
val control: Dp = 8.dp,
val sheet: Dp = 12.dp,
val pill: Dp = 999.dp,
val panel: Dp = 5.dp,
val control: Dp = 6.dp,
val button: Dp = 8.dp,
val sheet: Dp = 10.dp,
val pill: Dp = 12.dp,
)
@Immutable

View File

@@ -79,6 +79,9 @@ internal data class RealtimeToolRun(
val relaySessionId: String,
)
private const val REALTIME_AGENT_CONSULT_TOOL = "openclaw_agent_consult"
private const val REALTIME_AGENT_CONTROL_TOOL = "openclaw_agent_control"
private data class RealtimeToolCompletion(
val state: String,
val messageEl: JsonElement?,
@@ -88,7 +91,6 @@ class TalkModeManager internal constructor(
private val context: Context,
private val scope: CoroutineScope,
private val session: GatewaySession,
private val supportsChatSubscribe: Boolean,
private val isConnected: () -> Boolean,
private val onBeforeSpeak: suspend () -> Unit = {},
private val onAfterSpeak: suspend () -> Unit = {},
@@ -101,10 +103,11 @@ class TalkModeManager internal constructor(
private const val realtimeSampleRateHz = 24_000
private const val realtimeAudioFrameMs = 100
private const val listenWatchdogMs = 12_000L
private const val chatFinalWaitWithSubscribeMs = 45_000L
private const val chatFinalWaitWithoutSubscribeMs = 6_000L
private const val chatFinalWaitMs = 45_000L
private const val maxCachedRunCompletions = 128
private const val maxConversationEntries = 40
private const val realtimePlaybackBufferMs = 240
private const val realtimeUserFinalRewriteGraceMs = 1_500L
}
private val mainHandler = Handler(Looper.getMainLooper())
@@ -153,7 +156,6 @@ class TalkModeManager internal constructor(
private val completedRunsLock = Any()
private val completedRunStates = LinkedHashMap<String, Boolean>()
private val completedRunTexts = LinkedHashMap<String, String>()
private var chatSubscribedSessionKey: String? = null
private var configLoaded = false
private var executionMode = TalkModeExecutionMode.Native
private val startGeneration = AtomicLong(0L)
@@ -165,9 +167,13 @@ class TalkModeManager internal constructor(
private val pendingRealtimeToolCalls = LinkedHashSet<String>()
private val pendingRealtimeToolCompletions = LinkedHashMap<String, RealtimeToolCompletion>()
private var realtimeUserEntryId: String? = null
private var realtimeUserEntryAwaitingFinal = false
private var realtimeUserEntryAwaitingFinalStartedAtMs: Long? = null
private var realtimeAssistantEntryId: String? = null
private val realtimePlaybackLock = Any()
private var realtimeAudioTrack: AudioTrack? = null
private var realtimeAudioQueue: Channel<ByteArray>? = null
private var realtimeAudioWriterJob: Job? = null
private var realtimePlaybackIdleJob: Job? = null
@Volatile
@@ -207,11 +213,6 @@ class TalkModeManager internal constructor(
}
}
suspend fun ensureChatSubscribed() {
reloadConfig()
subscribeChatIfNeeded(session = session, sessionKey = mainSessionKey.ifBlank { "main" })
}
fun setMainSessionKey(sessionKey: String?) {
val trimmed = sessionKey?.trim().orEmpty()
if (trimmed.isEmpty()) return
@@ -363,7 +364,6 @@ class TalkModeManager internal constructor(
scope.launch {
try {
reloadConfig()
subscribeChatIfNeeded(session = session, sessionKey = mainSessionKey.ifBlank { "main" })
val startedAt = System.currentTimeMillis().toDouble() / 1000.0
val prompt = buildPrompt(command)
val runId = sendChat(prompt, session)
@@ -581,7 +581,6 @@ class TalkModeManager internal constructor(
_statusText.value = "Off"
stopRealtimeRelay()
stopSpeaking()
chatSubscribedSessionKey = null
pendingRunId = null
pendingFinal?.cancel()
pendingFinal = null
@@ -785,6 +784,7 @@ class TalkModeManager internal constructor(
}
"audio" -> {
if (realtimeOutputSuppressed) return
finishRealtimeConversationEntry(VoiceConversationRole.User)
val audioBase64 = obj["audioBase64"].asStringOrNull() ?: return
val bytes =
try {
@@ -799,16 +799,20 @@ class TalkModeManager internal constructor(
"mark" -> Unit
"transcript" -> {
val role = obj["role"].asStringOrNull()
val text = obj["text"].asStringOrNull()?.trim().orEmpty()
val isFinal = obj["final"].asBooleanOrNull() == true
if (text.isNotEmpty()) {
val text = realtimeTranscriptText(obj["text"].asStringOrNull(), isFinal)
var assistantText: String? = null
if (text != null) {
when (role) {
"user" -> upsertRealtimeConversation(VoiceConversationRole.User, text, isFinal)
"assistant" -> upsertRealtimeConversation(VoiceConversationRole.Assistant, text, isFinal)
"assistant" -> {
finishRealtimeConversationEntry(VoiceConversationRole.User)
assistantText = upsertRealtimeConversation(VoiceConversationRole.Assistant, text, isFinal)
}
}
}
if (role == "assistant" && text.isNotEmpty()) {
_lastAssistantText.value = text
if (assistantText != null) {
_lastAssistantText.value = assistantText.trim()
}
if (isFinal && role == "user") {
realtimeOutputSuppressed = false
@@ -824,6 +828,7 @@ class TalkModeManager internal constructor(
callId = callId,
name = name,
args = obj["args"],
forced = obj["forced"].asBooleanOrNull() == true,
)
}
"toolResult" -> Unit
@@ -849,6 +854,34 @@ class TalkModeManager internal constructor(
private fun playRealtimeAudio(bytes: ByteArray) {
if (!playbackEnabled || realtimeOutputSuppressed || bytes.isEmpty()) return
val queue = ensureRealtimeAudioQueue()
if (!queue.trySend(bytes).isSuccess) {
Log.w(tag, "realtime audio queue full")
}
}
private fun ensureRealtimeAudioQueue(): Channel<ByteArray> =
synchronized(realtimePlaybackLock) {
realtimeAudioQueue
?: Channel<ByteArray>(Channel.UNLIMITED).also { queue ->
realtimeAudioQueue = queue
realtimeAudioWriterJob =
scope.launch(Dispatchers.IO) {
for (chunk in queue) {
if (!playbackEnabled || realtimeOutputSuppressed || realtimeSessionId == null) continue
try {
writeRealtimeAudio(chunk)
} catch (err: CancellationException) {
throw err
} catch (err: Throwable) {
Log.w(tag, "realtime audio playback failed: ${err.message ?: err::class.java.simpleName}")
}
}
}
}
}
private fun writeRealtimeAudio(bytes: ByteArray) {
synchronized(realtimePlaybackLock) {
val track =
realtimeAudioTrack ?: run {
@@ -858,6 +891,12 @@ class TalkModeManager internal constructor(
AudioFormat.CHANNEL_OUT_MONO,
AudioFormat.ENCODING_PCM_16BIT,
)
val bufferSizeBytes =
maxOf(
minBuffer * 2,
realtimeSampleRateHz * 2 * realtimePlaybackBufferMs / 1000,
bytes.size * 4,
)
val created =
AudioTrack
.Builder()
@@ -875,16 +914,27 @@ class TalkModeManager internal constructor(
.setChannelMask(AudioFormat.CHANNEL_OUT_MONO)
.build(),
).setTransferMode(AudioTrack.MODE_STREAM)
.setBufferSizeInBytes(maxOf(minBuffer, bytes.size * 4))
.setBufferSizeInBytes(bufferSizeBytes)
.build()
created.play()
realtimeAudioTrack = created
created
}
var writtenBytes = 0
while (writtenBytes < bytes.size) {
val written = track.write(bytes, writtenBytes, bytes.size - writtenBytes)
if (written <= 0) {
Log.w(tag, "realtime audio write failed: $written")
break
}
writtenBytes += written
}
if (writtenBytes <= 0) return
if (track.playState != AudioTrack.PLAYSTATE_PLAYING) {
track.play()
}
_isSpeaking.value = true
_statusText.value = "Speaking…"
track.write(bytes, 0, bytes.size)
val durationMs = ((bytes.size / 2.0) / realtimeSampleRateHz * 1000.0).toLong()
val durationMs = ((writtenBytes / 2.0) / realtimeSampleRateHz * 1000.0).toLong()
val now = SystemClock.elapsedRealtime()
realtimePlaybackEndsAtMs = maxOf(now, realtimePlaybackEndsAtMs) + durationMs
scheduleRealtimePlaybackIdle()
@@ -910,6 +960,12 @@ class TalkModeManager internal constructor(
}
private fun stopRealtimePlayback() {
val audioQueue = realtimeAudioQueue
val audioWriterJob = realtimeAudioWriterJob
realtimeAudioQueue = null
realtimeAudioWriterJob = null
audioQueue?.close()
audioWriterJob?.cancel()
realtimePlaybackIdleJob?.cancel()
realtimePlaybackIdleJob = null
realtimePlaybackEndsAtMs = 0L
@@ -953,6 +1009,8 @@ class TalkModeManager internal constructor(
pendingRealtimeToolCalls.clear()
pendingRealtimeToolCompletions.clear()
realtimeUserEntryId = null
realtimeUserEntryAwaitingFinal = false
realtimeUserEntryAwaitingFinalStartedAtMs = null
realtimeAssistantEntryId = null
stopRealtimePlayback()
if (preserveStatus) {
@@ -981,11 +1039,19 @@ class TalkModeManager internal constructor(
callId: String,
name: String,
args: JsonElement?,
forced: Boolean = false,
) {
val relaySessionId = realtimeSessionId ?: return
pendingRealtimeToolCalls.add(callId)
scope.launch {
try {
if (name == REALTIME_AGENT_CONTROL_TOOL) {
submitRealtimeAgentControl(callId = callId, relaySessionId = relaySessionId, args = args)
return@launch
}
if (forced) {
submitRealtimeToolWorking(callId, relaySessionId)
}
val params =
buildJsonObject {
put("sessionKey", JsonPrimitive(mainSessionKey.ifBlank { "main" }))
@@ -1086,6 +1152,7 @@ class TalkModeManager internal constructor(
callId: String,
result: JsonObject,
sessionId: String? = realtimeSessionId,
options: JsonObject? = null,
) {
val activeSessionId = sessionId ?: return
val params =
@@ -1093,6 +1160,7 @@ class TalkModeManager internal constructor(
put("sessionId", JsonPrimitive(activeSessionId))
put("callId", JsonPrimitive(callId))
put("result", result)
if (options != null) put("options", options)
}
try {
session.request("talk.session.submitToolResult", params.toString(), timeoutMs = 15_000)
@@ -1102,27 +1170,152 @@ class TalkModeManager internal constructor(
}
}
private suspend fun submitRealtimeToolWorking(
callId: String,
sessionId: String,
) {
submitRealtimeToolResult(
callId = callId,
sessionId = sessionId,
result =
buildJsonObject {
put("status", JsonPrimitive("working"))
put("tool", JsonPrimitive(REALTIME_AGENT_CONSULT_TOOL))
put(
"message",
JsonPrimitive(
"Tell the person briefly that you are checking, then wait for the final OpenClaw result before answering with the actual result.",
),
)
},
options = buildJsonObject { put("willContinue", JsonPrimitive(true)) },
)
}
private suspend fun submitRealtimeAgentControl(
callId: String,
relaySessionId: String,
args: JsonElement?,
) {
val argsObject = args.asObjectOrNull()
val text =
argsObject
?.get("text")
.asStringOrNull()
?.trim()
.orEmpty()
val mode =
argsObject
?.get("mode")
.asStringOrNull()
?.trim()
val params =
buildJsonObject {
put("sessionId", JsonPrimitive(relaySessionId))
put("sessionKey", JsonPrimitive(mainSessionKey.ifBlank { "main" }))
put("text", JsonPrimitive(text.ifEmpty { "status" }))
if (!mode.isNullOrEmpty()) put("mode", JsonPrimitive(mode))
}
val response = session.request("talk.session.steer", params.toString(), timeoutMs = 15_000)
val result = json.parseToJsonElement(response).asObjectOrNull()
if (result != null) {
submitRealtimeToolResult(callId = callId, result = result, sessionId = relaySessionId)
} else {
submitRealtimeToolError(callId, "control call returned no result", relaySessionId)
}
}
private fun upsertRealtimeConversation(
role: VoiceConversationRole,
text: String,
isFinal: Boolean,
) {
val entryId =
): String {
var entryId =
when (role) {
VoiceConversationRole.User -> realtimeUserEntryId
VoiceConversationRole.Assistant -> realtimeAssistantEntryId
}
if (role == VoiceConversationRole.Assistant) {
finishRealtimeConversationEntry(VoiceConversationRole.User)
}
val shouldStartNewUserEntry =
role == VoiceConversationRole.User &&
entryId != null &&
shouldStartNewRealtimeUserEntry(entryId, text, isFinal)
if (
role == VoiceConversationRole.User &&
(entryId == null || shouldStartNewUserEntry)
) {
finishRealtimeConversationEntry(VoiceConversationRole.Assistant)
}
if (shouldStartNewUserEntry) {
finishRealtimeConversationEntry(VoiceConversationRole.User)
entryId = null
realtimeUserEntryAwaitingFinal = false
realtimeUserEntryAwaitingFinalStartedAtMs = null
}
var resolvedText: String
val resolvedEntryId =
if (entryId == null) {
appendConversation(role = role, text = text, isStreaming = !isFinal)
resolvedText = text.trimStart()
appendConversation(role = role, text = resolvedText, isStreaming = !isFinal)
} else {
updateConversationEntry(id = entryId, text = text, isStreaming = !isFinal)
resolvedText = updateConversationEntry(id = entryId, text = text, isStreaming = !isFinal)
entryId
}
when (role) {
VoiceConversationRole.User -> realtimeUserEntryId = if (isFinal) null else resolvedEntryId
VoiceConversationRole.User -> {
realtimeUserEntryId = if (isFinal) null else resolvedEntryId
realtimeUserEntryAwaitingFinal = false
realtimeUserEntryAwaitingFinalStartedAtMs = null
}
VoiceConversationRole.Assistant -> realtimeAssistantEntryId = if (isFinal) null else resolvedEntryId
}
return resolvedText
}
private fun finishRealtimeConversationEntry(role: VoiceConversationRole) {
val entryId =
when (role) {
VoiceConversationRole.User -> realtimeUserEntryId
VoiceConversationRole.Assistant -> realtimeAssistantEntryId
} ?: return
val current = _conversation.value
val targetIndex = current.indexOfFirst { it.id == entryId }
if (targetIndex >= 0 && current[targetIndex].isStreaming) {
val updated = current.toMutableList()
updated[targetIndex] = current[targetIndex].copy(isStreaming = false)
_conversation.value = updated
if (role == VoiceConversationRole.User) {
realtimeUserEntryAwaitingFinal = true
realtimeUserEntryAwaitingFinalStartedAtMs = SystemClock.elapsedRealtime()
}
}
when (role) {
VoiceConversationRole.User -> Unit
VoiceConversationRole.Assistant -> realtimeAssistantEntryId = null
}
}
private fun shouldStartNewRealtimeUserEntry(
entryId: String,
incoming: String,
isFinal: Boolean,
): Boolean {
val entry = _conversation.value.firstOrNull { it.id == entryId } ?: return false
if (entry.isStreaming) return false
val existing = entry.text
if (existing.isBlank() || incoming.isBlank()) return false
if (incoming.firstOrNull()?.isWhitespace() == true) return false
if (incoming == existing || incoming.startsWith(existing) || existing.endsWith(incoming)) return false
if (isFinal && realtimeUserEntryAwaitingFinal) {
val elapsedMs =
realtimeUserEntryAwaitingFinalStartedAtMs?.let { SystemClock.elapsedRealtime() - it } ?: Long.MAX_VALUE
if (elapsedMs <= realtimeUserFinalRewriteGraceMs && looksLikeTranscriptReplacement(existing, incoming)) {
return false
}
}
return true
}
private fun appendConversation(
@@ -1141,7 +1334,7 @@ class TalkModeManager internal constructor(
id: String,
text: String,
isStreaming: Boolean,
) {
): String {
val current = _conversation.value
val targetIndex =
when {
@@ -1149,14 +1342,112 @@ class TalkModeManager internal constructor(
current[current.lastIndex].id == id -> current.lastIndex
else -> current.indexOfFirst { it.id == id }
}
if (targetIndex < 0) return
if (targetIndex < 0) return text
val entry = current[targetIndex]
if (entry.text == text && entry.isStreaming == isStreaming) return
val updatedText = mergeRealtimeTranscriptText(entry.text, text, isFinal = !isStreaming)
if (entry.text == updatedText && entry.isStreaming == isStreaming) return entry.text
val updated = current.toMutableList()
updated[targetIndex] = entry.copy(text = text, isStreaming = isStreaming)
updated[targetIndex] = entry.copy(text = updatedText, isStreaming = isStreaming)
_conversation.value = updated
return updatedText
}
private fun realtimeTranscriptText(
rawText: String?,
isFinal: Boolean,
): String? {
val text = rawText ?: return null
return text.takeIf { if (isFinal) it.isNotBlank() else it.isNotEmpty() }
}
private fun mergeRealtimeTranscriptText(
existing: String,
incoming: String,
isFinal: Boolean,
): String {
if (existing.isBlank()) return incoming.trimStart()
if (incoming.isEmpty()) return existing
if (incoming == existing || existing.endsWith(incoming)) return existing
if (incoming.startsWith(existing)) return incoming
if (incoming.firstOrNull()?.isWhitespace() == true) return existing + incoming
if (isFinal && looksLikeTranscriptReplacement(existing, incoming)) return incoming
val overlap = findTranscriptTextOverlap(existing, incoming)
val suffix = if (overlap > 0) incoming.drop(overlap) else incoming
if (suffix.isEmpty()) return existing
val separator =
if (overlap > 0 || !shouldInsertTranscriptSpace(existing, suffix)) {
""
} else {
" "
}
return existing + separator + suffix
}
private fun looksLikeTranscriptReplacement(
existing: String,
incoming: String,
): Boolean {
val existingWords = transcriptWords(existing)
val incomingWords = transcriptWords(incoming)
if (existingWords.isEmpty() || incomingWords.isEmpty()) return false
if (existingWords[0] != incomingWords[0]) return false
if (existingWords.size > 1 && incomingWords.size > 1 && existingWords[1] == incomingWords[1]) return true
val existingText = normalizeTranscriptText(existing)
val incomingText = normalizeTranscriptText(incoming)
val commonPrefix = commonPrefixLength(existingText, incomingText)
val shortest = minOf(existingText.length, incomingText.length)
return commonPrefix >= 6 && commonPrefix.toDouble() / maxOf(1, shortest).toDouble() >= 0.45
}
private fun transcriptWords(value: String): List<String> =
Regex("""[\p{L}\p{N}]+""")
.findAll(value.lowercase(Locale.ROOT))
.map { it.value }
.toList()
private fun normalizeTranscriptText(value: String): String = value.lowercase(Locale.ROOT).replace(Regex("""\s+"""), " ").trim()
private fun commonPrefixLength(
left: String,
right: String,
): Int {
val max = minOf(left.length, right.length)
var index = 0
while (index < max && left[index] == right[index]) {
index += 1
}
return index
}
private fun findTranscriptTextOverlap(
existing: String,
incoming: String,
): Int {
val base = existing.lowercase(Locale.ROOT)
val next = incoming.lowercase(Locale.ROOT)
val max = minOf(base.length, next.length)
for (length in max downTo 3) {
if (base.endsWith(next.take(length))) {
return length
}
}
return 0
}
private fun shouldInsertTranscriptSpace(
existing: String,
incoming: String,
): Boolean {
val last = existing.lastOrNull() ?: return false
val first = incoming.firstOrNull() ?: return false
if (last.isWhitespace() || first.isWhitespace()) return false
return first.isLetterOrDigit() &&
(last.isLetterOrDigit() || transcriptSpaceAfterPunctuation.contains(last))
}
private val transcriptSpaceAfterPunctuation =
setOf('.', '!', '?', ',', ':', ';', ')', ']', '}', '"', '\'', '', '”')
private fun startListeningInternal(markListening: Boolean) {
val r = recognizer ?: return
val intent =
@@ -1289,7 +1580,6 @@ class TalkModeManager internal constructor(
try {
val startedAt = System.currentTimeMillis().toDouble() / 1000.0
subscribeChatIfNeeded(session = session, sessionKey = mainSessionKey)
Log.d(tag, "chat.send start sessionKey=${mainSessionKey.ifBlank { "main" }} chars=${prompt.length}")
val runId = sendChat(prompt, session)
Log.d(tag, "chat.send ok runId=$runId")
@@ -1348,23 +1638,6 @@ class TalkModeManager internal constructor(
return payload
}
private suspend fun subscribeChatIfNeeded(
session: GatewaySession,
sessionKey: String,
) {
if (!supportsChatSubscribe) return
val key = sessionKey.trim()
if (key.isEmpty()) return
if (chatSubscribedSessionKey == key) return
val sent = session.sendNodeEvent("chat.subscribe", """{"sessionKey":"$key"}""")
if (sent) {
chatSubscribedSessionKey = key
Log.d(tag, "chat.subscribe ok sessionKey=$key")
} else {
Log.w(tag, "chat.subscribe failed sessionKey=$key")
}
}
private fun buildPrompt(transcript: String): String {
val lines =
mutableListOf(
@@ -1418,10 +1691,9 @@ class TalkModeManager internal constructor(
consumeRunCompletion(runId)?.let { return it }
val timeoutMs = if (supportsChatSubscribe) chatFinalWaitWithSubscribeMs else chatFinalWaitWithoutSubscribeMs
val result =
try {
withTimeout(timeoutMs) { deferred.await() }
withTimeout(chatFinalWaitMs) { deferred.await() }
} catch (_: TimeoutCancellationException) {
false
}

View File

@@ -15,6 +15,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Test
@@ -120,6 +121,31 @@ class GatewayBootstrapAuthTest {
)
}
@Test
fun nodeConnectStartsOperatorAfterBootstrapHandoffWhenOperatorWasConnecting() {
val app = RuntimeEnvironment.getApplication()
val securePrefs =
app.getSharedPreferences(
"openclaw.node.secure.test.${UUID.randomUUID()}",
android.content.Context.MODE_PRIVATE,
)
val prefs = SecurePrefs(app, securePrefsOverride = securePrefs)
val runtime = NodeRuntime(app, prefs)
val deviceId = DeviceIdentityStore(app).loadOrCreate().deviceId
DeviceAuthStore(prefs).saveToken(deviceId, "operator", "bootstrap-operator-token")
writeField(runtime, "operatorStatusText", "Connecting…")
invokeMaybeStartOperatorSessionAfterNodeConnect(
runtime = runtime,
endpoint = GatewayEndpoint.manual(host = "127.0.0.1", port = 18789),
auth = NodeRuntime.GatewayConnectAuth(token = null, bootstrapToken = "setup-bootstrap-token", password = null),
)
val desired = desiredConnection(runtime, "operatorSession")
assertNotNull(desired)
assertNull(readField<String?>(desired!!, "bootstrapToken"))
}
@Test
fun resolveGatewayConnectAuth_prefersExplicitSetupAuthOverStoredPrefs() {
val app = RuntimeEnvironment.getApplication()
@@ -358,11 +384,52 @@ class GatewayBootstrapAuthTest {
runtime: NodeRuntime,
sessionFieldName: String,
): String? {
val session = readField<GatewaySession>(runtime, sessionFieldName)
val desired = readField<Any?>(session, "desired") ?: return null
val desired = desiredConnection(runtime, sessionFieldName) ?: return null
return readField(desired, "bootstrapToken")
}
private fun desiredConnection(
runtime: NodeRuntime,
sessionFieldName: String,
): Any? {
val session = readField<GatewaySession>(runtime, sessionFieldName)
return readField(session, "desired")
}
private fun invokeMaybeStartOperatorSessionAfterNodeConnect(
runtime: NodeRuntime,
endpoint: GatewayEndpoint,
auth: NodeRuntime.GatewayConnectAuth,
) {
val method =
runtime.javaClass.getDeclaredMethod(
"maybeStartOperatorSessionAfterNodeConnect",
GatewayEndpoint::class.java,
NodeRuntime.GatewayConnectAuth::class.java,
)
method.isAccessible = true
method.invoke(runtime, endpoint, auth)
}
private fun writeField(
target: Any,
name: String,
value: Any?,
) {
var type: Class<*>? = target.javaClass
while (type != null) {
try {
val field: Field = type.getDeclaredField(name)
field.isAccessible = true
field.set(target, value)
return
} catch (_: NoSuchFieldException) {
type = type.superclass
}
}
error("Field $name not found on ${target.javaClass.name}")
}
private fun waitForDesiredBootstrapToken(
runtime: NodeRuntime,
sessionFieldName: String,

View File

@@ -1,6 +1,8 @@
package ai.openclaw.app.chat
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
class ChatControllerSessionPolicyTest {
@@ -29,4 +31,32 @@ class ChatControllerSessionPolicyTest {
assertEquals("custom", state.currentSessionKey)
assertEquals("agent:ops:node-new", state.appliedMainSessionKey)
}
@Test
fun staleHistoryLoadCannotApplyAfterSessionSwitch() {
assertTrue(
isCurrentHistoryLoad(
requestedSessionKey = "agent:one",
currentSessionKey = "agent:one",
requestGeneration = 2,
activeGeneration = 2,
),
)
assertFalse(
isCurrentHistoryLoad(
requestedSessionKey = "agent:old",
currentSessionKey = "agent:new",
requestGeneration = 1,
activeGeneration = 2,
),
)
assertFalse(
isCurrentHistoryLoad(
requestedSessionKey = "agent:new",
currentSessionKey = "agent:new",
requestGeneration = 1,
activeGeneration = 2,
),
)
}
}

View File

@@ -0,0 +1,49 @@
package ai.openclaw.app.chat
import kotlinx.serialization.json.Json
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Test
class ChatMessageContentParsingTest {
@Test
fun dropsInternalToolBlocksFromDisplayHistory() {
val content =
Json.parseToJsonElement(
"""{"type":"toolResult","content":"large internal output"}""",
)
assertNull(parseChatMessageContent(content))
}
@Test
fun parsesCodexTextBlocksAsVisibleText() {
val content =
Json.parseToJsonElement(
"""{"type":"output_text","text":"Done."}""",
)
assertEquals(ChatMessageContent(type = "text", text = "Done."), parseChatMessageContent(content))
}
@Test
fun parsesImageBlocksOnlyWhenInlineContentExists() {
val image =
Json.parseToJsonElement(
"""{"type":"image","mimeType":"image/png","fileName":"chart.png","content":"abc123"}""",
)
val managedImage =
Json.parseToJsonElement(
"""{"type":"image","mimeType":"image/png","fileName":"chart.png","url":"/api/chat/media/outgoing/main/id"}""",
)
assertEquals(
ChatMessageContent(type = "image", mimeType = "image/png", fileName = "chart.png", base64 = "abc123"),
parseChatMessageContent(image),
)
assertEquals(
ChatMessageContent(type = "image", mimeType = "image/png", fileName = "chart.png", base64 = null),
parseChatMessageContent(managedImage),
)
}
}

View File

@@ -10,6 +10,7 @@ import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeout
import kotlinx.coroutines.withTimeoutOrNull
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
@@ -27,6 +28,7 @@ import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.RuntimeEnvironment
import org.robolectric.annotation.Config
import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.atomic.AtomicReference
@@ -123,6 +125,58 @@ class GatewaySessionInvokeTest {
}
}
@Test
fun eventsAreDispatchedInWebSocketFrameOrder() =
runBlocking {
val json = testJson()
val connected = CompletableDeferred<Unit>()
val firstEventStarted = CompletableDeferred<Unit>()
val releaseFirstEvent = CompletableDeferred<Unit>()
val secondEventHandled = CompletableDeferred<Unit>()
val events = CopyOnWriteArrayList<String>()
val lastDisconnect = AtomicReference("")
val server =
startGatewayServer(json) { webSocket, id, method, _ ->
if (method == "connect") {
webSocket.send(connectResponseFrame(id))
webSocket.send("""{"type":"event","event":"voice.first","payload":{}}""")
webSocket.send("""{"type":"event","event":"voice.second","payload":{}}""")
webSocket.close(1000, "done")
}
}
val harness =
createNodeHarness(
connected = connected,
lastDisconnect = lastDisconnect,
onEvent = { event, _ ->
if (event == "voice.first") {
firstEventStarted.complete(Unit)
runBlocking { releaseFirstEvent.await() }
}
events += event
if (event == "voice.second") {
secondEventHandled.complete(Unit)
}
},
) { GatewaySession.InvokeResult.ok("""{"handled":true}""") }
try {
connectNodeSession(harness.session, server.port)
awaitConnectedOrThrow(connected, lastDisconnect, server)
withTimeout(TEST_TIMEOUT_MS) { firstEventStarted.await() }
assertNull(withTimeoutOrNull(200) { secondEventHandled.await() })
releaseFirstEvent.complete(Unit)
withTimeout(TEST_TIMEOUT_MS) { secondEventHandled.await() }
assertEquals(listOf("voice.first", "voice.second"), events.toList())
} finally {
releaseFirstEvent.complete(Unit)
shutdownHarness(harness, server)
}
}
@Test
fun connect_usesBootstrapTokenWhenSharedAndDeviceTokensAreAbsent() =
runBlocking {
@@ -212,6 +266,126 @@ class GatewaySessionInvokeTest {
}
}
@Test
fun connect_reusesStoredDeviceTokenScopes() =
runBlocking {
val json = testJson()
val connected = CompletableDeferred<Unit>()
val connectParams = CompletableDeferred<JsonObject>()
val lastDisconnect = AtomicReference("")
val server =
startGatewayServer(json) { webSocket, id, method, frame ->
if (method == "connect") {
if (!connectParams.isCompleted) {
connectParams.complete(frame["params"]!!.jsonObject)
}
webSocket.send(connectResponseFrame(id))
webSocket.close(1000, "done")
}
}
val harness =
createNodeHarness(
connected = connected,
lastDisconnect = lastDisconnect,
) { GatewaySession.InvokeResult.ok("""{"handled":true}""") }
try {
val deviceId = DeviceIdentityStore(RuntimeEnvironment.getApplication()).loadOrCreate().deviceId
harness.deviceAuthStore.saveToken(
deviceId = deviceId,
role = "operator",
token = "operator-device-token",
scopes = listOf("operator.pairing", "operator.write"),
)
connectNodeSession(
session = harness.session,
port = server.port,
token = null,
role = "operator",
scopes = listOf("operator.approvals", "operator.read", "operator.write"),
)
awaitConnectedOrThrow(connected, lastDisconnect, server)
val params = withTimeout(TEST_TIMEOUT_MS) { connectParams.await() }
assertEquals(
"operator-device-token",
params["auth"]
?.jsonObject
?.get("token")
?.jsonPrimitive
?.content,
)
assertEquals(listOf("operator.pairing", "operator.write"), params.scopes())
} finally {
shutdownHarness(harness, server)
}
}
@Test
fun bootstrapConnect_filtersOperatorHandoffScopesFromConnectRequest() =
runBlocking {
val json = testJson()
val connected = CompletableDeferred<Unit>()
val connectParams = CompletableDeferred<JsonObject>()
val lastDisconnect = AtomicReference("")
val server =
startGatewayServer(json) { webSocket, id, method, frame ->
if (method == "connect") {
if (!connectParams.isCompleted) {
connectParams.complete(frame["params"]!!.jsonObject)
}
webSocket.send(connectResponseFrame(id))
webSocket.close(1000, "done")
}
}
val harness =
createNodeHarness(
connected = connected,
lastDisconnect = lastDisconnect,
) { GatewaySession.InvokeResult.ok("""{"handled":true}""") }
try {
connectNodeSession(
session = harness.session,
port = server.port,
token = null,
bootstrapToken = "setup-bootstrap-token",
role = "operator",
scopes =
listOf(
"operator.approvals",
"operator.pairing",
"operator.read",
"operator.write",
),
)
awaitConnectedOrThrow(connected, lastDisconnect, server)
val params = withTimeout(TEST_TIMEOUT_MS) { connectParams.await() }
assertEquals(
"setup-bootstrap-token",
params["auth"]
?.jsonObject
?.get("bootstrapToken")
?.jsonPrimitive
?.content,
)
assertEquals(
listOf(
"operator.approvals",
"operator.read",
"operator.write",
),
params.scopes(),
)
} finally {
shutdownHarness(harness, server)
}
}
@Test
fun connect_retriesWithStoredDeviceTokenAfterSharedTokenMismatch() =
runBlocking {
@@ -364,10 +538,7 @@ class GatewaySessionInvokeTest {
assertEquals("bootstrap-node-token", nodeEntry?.token)
assertEquals(emptyList<String>(), nodeEntry?.scopes)
assertEquals("bootstrap-operator-token", operatorEntry?.token)
assertEquals(
listOf("operator.approvals", "operator.pairing", "operator.read", "operator.write"),
operatorEntry?.scopes,
)
assertEquals(listOf("operator.approvals", "operator.read", "operator.write"), operatorEntry?.scopes)
} finally {
shutdownHarness(harness, server)
}
@@ -629,9 +800,15 @@ class GatewaySessionInvokeTest {
private fun testJson(): Json = Json { ignoreUnknownKeys = true }
private fun JsonObject.scopes(): List<String> =
(this["scopes"] as? JsonArray)
?.map { it.jsonPrimitive.content }
?: emptyList()
private fun createNodeHarness(
connected: CompletableDeferred<Unit>,
lastDisconnect: AtomicReference<String>,
onEvent: (event: String, payloadJson: String?) -> Unit = { _, _ -> },
onInvoke: (GatewaySession.InvokeRequest) -> GatewaySession.InvokeResult,
): NodeHarness {
val app = RuntimeEnvironment.getApplication()
@@ -648,7 +825,7 @@ class GatewaySessionInvokeTest {
onDisconnected = { message ->
lastDisconnect.set(message)
},
onEvent = { _, _ -> },
onEvent = onEvent,
onInvoke = onInvoke,
)
@@ -660,6 +837,8 @@ class GatewaySessionInvokeTest {
port: Int,
token: String? = "test-token",
bootstrapToken: String? = null,
role: String = "node",
scopes: List<String> = listOf("node:invoke"),
) {
session.connect(
endpoint =
@@ -675,8 +854,8 @@ class GatewaySessionInvokeTest {
password = null,
options =
GatewayConnectOptions(
role = "node",
scopes = listOf("node:invoke"),
role = role,
scopes = scopes,
caps = emptyList(),
commands = emptyList(),
permissions = emptyMap(),
@@ -686,7 +865,7 @@ class GatewaySessionInvokeTest {
displayName = "Android Test",
version = "1.0.0-test",
platform = "android",
mode = "node",
mode = role,
instanceId = "android-test-instance",
deviceFamily = "android",
modelIdentifier = "test",

View File

@@ -375,7 +375,6 @@ class ConnectionManagerTest {
assertEquals(
listOf(
"operator.approvals",
"operator.pairing",
"operator.read",
"operator.write",
),

View File

@@ -1,5 +1,6 @@
package ai.openclaw.app.ui
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
@@ -24,4 +25,64 @@ class OnboardingFlowLogicTest {
fun allowsFinishOnlyWhenOperatorAndNodeAreConnected() {
assertTrue(canFinishOnboarding(isConnected = true, isNodeConnected = true))
}
@Test
fun showsPairingStateForPairingRequiredGatewayStatus() {
assertEquals(
GatewayRecoveryUiState.Pairing,
gatewayRecoveryUiState(
ready = false,
statusText = "Gateway error: pairing required; approval in progress",
connectSettling = false,
),
)
}
@Test
fun showsConnectedStateWhenGatewayBecomesReady() {
assertEquals(
GatewayRecoveryUiState.Connected,
gatewayRecoveryUiState(
ready = true,
statusText = "Gateway error: pairing required",
connectSettling = false,
),
)
}
@Test
fun showsFinishingStateWhileGatewayConnectionSettles() {
assertEquals(
GatewayRecoveryUiState.Finishing,
gatewayRecoveryUiState(
ready = false,
statusText = "Offline",
connectSettling = true,
),
)
}
@Test
fun showsFinishingStateForPartialGatewayConnection() {
assertEquals(
GatewayRecoveryUiState.Finishing,
gatewayRecoveryUiState(
ready = false,
statusText = "Connected (node offline)",
connectSettling = false,
),
)
}
@Test
fun showsConnectionIssueForNonPairingFailure() {
assertEquals(
GatewayRecoveryUiState.Failed,
gatewayRecoveryUiState(
ready = false,
statusText = "Gateway error: connection refused",
connectSettling = false,
),
)
}
}

View File

@@ -139,6 +139,194 @@ class TalkModeManagerTest {
)
}
@Test
fun realtimeTranscriptDeltasAccumulateVoiceConversation() {
val manager = createManager()
setPrivateField(manager, "realtimeSessionId", "relay-1")
manager.handleGatewayEvent("talk.event", realtimeTranscriptPayload(role = "assistant", text = "The"))
manager.handleGatewayEvent("talk.event", realtimeTranscriptPayload(role = "assistant", text = " answer"))
val entry = manager.conversation.value.single()
assertEquals("The answer", entry.text)
assertTrue(entry.isStreaming)
}
@Test
fun realtimeTranscriptFragmentsInsertWordSpacing() {
val manager = createManager()
setPrivateField(manager, "realtimeSessionId", "relay-1")
manager.handleGatewayEvent("talk.event", realtimeTranscriptPayload(role = "user", text = "Turn off"))
manager.handleGatewayEvent("talk.event", realtimeTranscriptPayload(role = "user", text = "the lights"))
val entry = manager.conversation.value.single()
assertEquals("Turn off the lights", entry.text)
assertTrue(entry.isStreaming)
}
@Test
fun realtimeTranscriptFragmentsInsertSpacingAfterPunctuation() {
val manager = createManager()
setPrivateField(manager, "realtimeSessionId", "relay-1")
manager.handleGatewayEvent("talk.event", realtimeTranscriptPayload(role = "assistant", text = "Ready."))
manager.handleGatewayEvent("talk.event", realtimeTranscriptPayload(role = "assistant", text = "What next?"))
val entry = manager.conversation.value.single()
assertEquals("Ready. What next?", entry.text)
assertTrue(entry.isStreaming)
}
@Test
fun realtimeFinalTranscriptCanCompleteDeltaText() {
val manager = createManager()
setPrivateField(manager, "realtimeSessionId", "relay-1")
manager.handleGatewayEvent("talk.event", realtimeTranscriptPayload(role = "assistant", text = "The"))
manager.handleGatewayEvent(
"talk.event",
realtimeTranscriptPayload(role = "assistant", text = " answer", final = true),
)
val entry = manager.conversation.value.single()
assertEquals("The answer", entry.text)
assertFalse(entry.isStreaming)
}
@Test
fun realtimeAssistantOutputSeparatesNextUserBubble() {
val manager = createManager()
setPrivateField(manager, "realtimeSessionId", "relay-1")
manager.handleGatewayEvent("talk.event", realtimeTranscriptPayload(role = "user", text = "First request"))
manager.handleGatewayEvent("talk.event", realtimeTranscriptPayload(role = "assistant", text = "Checking"))
manager.handleGatewayEvent("talk.event", realtimeTranscriptPayload(role = "user", text = "Second request"))
val entries = manager.conversation.value
assertEquals(3, entries.size)
assertEquals(VoiceConversationRole.User, entries[0].role)
assertEquals("First request", entries[0].text)
assertFalse(entries[0].isStreaming)
assertEquals(VoiceConversationRole.Assistant, entries[1].role)
assertEquals("Checking", entries[1].text)
assertFalse(entries[1].isStreaming)
assertEquals(VoiceConversationRole.User, entries[2].role)
assertEquals("Second request", entries[2].text)
assertTrue(entries[2].isStreaming)
}
@Test
fun realtimeUserTranscriptRewriteStaysInSameBubble() {
val manager = createManager()
setPrivateField(manager, "realtimeSessionId", "relay-1")
manager.handleGatewayEvent("talk.event", realtimeTranscriptPayload(role = "user", text = "Can you tack"))
manager.handleGatewayEvent("talk.event", realtimeTranscriptPayload(role = "user", text = "Can you check?", final = true))
val entry = manager.conversation.value.single()
assertEquals(VoiceConversationRole.User, entry.role)
assertEquals("Can you check?", entry.text)
assertFalse(entry.isStreaming)
}
@Test
fun realtimeLateFinalUserTranscriptRewritesBubbleAfterAssistantStarts() {
val manager = createManager()
setPrivateField(manager, "realtimeSessionId", "relay-1")
manager.handleGatewayEvent("talk.event", realtimeTranscriptPayload(role = "user", text = "Can you tack"))
manager.handleGatewayEvent("talk.event", realtimeTranscriptPayload(role = "assistant", text = "Checking"))
manager.handleGatewayEvent("talk.event", realtimeTranscriptPayload(role = "user", text = "Can you check?", final = true))
val entries = manager.conversation.value
assertEquals(2, entries.size)
assertEquals(VoiceConversationRole.User, entries[0].role)
assertEquals("Can you check?", entries[0].text)
assertFalse(entries[0].isStreaming)
assertEquals(VoiceConversationRole.Assistant, entries[1].role)
assertEquals("Checking", entries[1].text)
}
@Test
fun realtimeFinalNextUserAfterAssistantStartsCreatesNewBubble() {
val manager = createManager()
setPrivateField(manager, "realtimeSessionId", "relay-1")
manager.handleGatewayEvent("talk.event", realtimeTranscriptPayload(role = "user", text = "First request"))
manager.handleGatewayEvent("talk.event", realtimeTranscriptPayload(role = "assistant", text = "Checking"))
manager.handleGatewayEvent("talk.event", realtimeTranscriptPayload(role = "user", text = "Second request", final = true))
val entries = manager.conversation.value
assertEquals(3, entries.size)
assertEquals(VoiceConversationRole.User, entries[0].role)
assertEquals("First request", entries[0].text)
assertEquals(VoiceConversationRole.Assistant, entries[1].role)
assertEquals("Checking", entries[1].text)
assertEquals(VoiceConversationRole.User, entries[2].role)
assertEquals("Second request", entries[2].text)
assertFalse(entries[2].isStreaming)
}
@Test
fun realtimeAlternatingTurnsStayInSeparateBubbles() {
val manager = createManager()
setPrivateField(manager, "realtimeSessionId", "relay-1")
manager.handleGatewayEvent("talk.event", realtimeTranscriptPayload(role = "user", text = "Hey, what time is it?", final = true))
manager.handleGatewayEvent(
"talk.event",
realtimeTranscriptPayload(
role = "assistant",
text = "Let me look into that for you. It's currently 7:55 PM UTC.",
final = true,
),
)
manager.handleGatewayEvent("talk.event", realtimeTranscriptPayload(role = "user", text = "How's it going?", final = true))
manager.handleGatewayEvent(
"talk.event",
realtimeTranscriptPayload(
role = "assistant",
text = "Great! Ready for the next task. What can I do for you?",
final = true,
),
)
manager.handleGatewayEvent("talk.event", realtimeTranscriptPayload(role = "user", text = "Turn on the basement lights", final = true))
manager.handleGatewayEvent(
"talk.event",
realtimeTranscriptPayload(
role = "assistant",
text = "Got it, let me check on that.",
final = true,
),
)
val entries = manager.conversation.value
assertEquals(6, entries.size)
assertEquals(VoiceConversationRole.User, entries[0].role)
assertEquals("Hey, what time is it?", entries[0].text)
assertEquals(VoiceConversationRole.Assistant, entries[1].role)
assertEquals("Let me look into that for you. It's currently 7:55 PM UTC.", entries[1].text)
assertEquals(VoiceConversationRole.User, entries[2].role)
assertEquals("How's it going?", entries[2].text)
assertEquals(VoiceConversationRole.Assistant, entries[3].role)
assertEquals("Great! Ready for the next task. What can I do for you?", entries[3].text)
assertEquals(VoiceConversationRole.User, entries[4].role)
assertEquals("Turn on the basement lights", entries[4].text)
assertEquals(VoiceConversationRole.Assistant, entries[5].role)
assertEquals("Got it, let me check on that.", entries[5].text)
assertTrue(entries.none { it.isStreaming })
}
@Test
@OptIn(ExperimentalCoroutinesApi::class)
fun realtimeStartWithoutGatewayTurnsTalkOff() =
@@ -230,22 +418,21 @@ class TalkModeManagerTest {
@Test
@OptIn(ExperimentalCoroutinesApi::class)
fun chatFinalWaitWithoutSubscribeUsesShortTimeout() =
fun chatFinalWaitUsesGatewayEventTimeout() =
runTest {
val manager = createManager(scope = this, supportsChatSubscribe = false)
val manager = createManager(scope = this)
setPrivateField(manager, "pendingRunId", "run-missing-final")
setPrivateField(manager, "pendingFinal", CompletableDeferred<Boolean>())
assertFalse(manager.waitForChatFinal("run-missing-final"))
assertEquals(6_000, currentTime)
assertEquals(45_000, currentTime)
}
private fun createManager(
talkSpeakClient: TalkSpeechSynthesizing = TalkSpeakClient(),
talkAudioPlayer: TalkAudioPlaying? = null,
scope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default),
supportsChatSubscribe: Boolean = false,
isConnected: () -> Boolean = { true },
onStoppedByRelay: () -> Unit = {},
): TalkModeManager {
@@ -264,7 +451,6 @@ class TalkModeManagerTest {
context = app,
scope = scope,
session = session,
supportsChatSubscribe = supportsChatSubscribe,
isConnected = isConnected,
onStoppedByRelay = onStoppedByRelay,
talkSpeakClient = talkSpeakClient,

View File

@@ -52,7 +52,6 @@ struct OpenClawLiveActivity: Widget {
.padding(.vertical, 8)
}
@ViewBuilder
private func trailingView(state: OpenClawActivityAttributes.ContentState) -> some View {
self.statusIcon(state: state)
.font(.system(size: 16, weight: .semibold))
@@ -65,7 +64,6 @@ struct OpenClawLiveActivity: Widget {
.frame(width: 6, height: 6)
}
@ViewBuilder
private func compactStatusIcon(state: OpenClawActivityAttributes.ContentState) -> some View {
self.statusIcon(state: state)
.font(.system(size: 12, weight: .semibold))

View File

@@ -1,9 +1,23 @@
# OpenClaw iOS Changelog
## 2026.5.25 - 2026-05-25
Maintenance update for the current OpenClaw release.
## 2026.5.24 - 2026-05-24
Maintenance update for the current OpenClaw release.
## 2026.5.22 - 2026-05-22
Maintenance update for the current OpenClaw release.
## 2026.5.21 - 2026-05-21
Maintenance update for the current OpenClaw release.
- Added realtime Gateway Talk relay support for iOS voice sessions, including OpenAI realtime provider and voice selection controls. Thanks @Solvely-Colin.
## 2026.5.20 - 2026-05-20
Maintenance update for the current OpenClaw 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.21
OPENCLAW_MARKETING_VERSION = 2026.5.21
OPENCLAW_IOS_VERSION = 2026.5.25
OPENCLAW_MARKETING_VERSION = 2026.5.25
OPENCLAW_BUILD_VERSION = 1
#include? "../build/Version.xcconfig"

View File

@@ -20,6 +20,93 @@ import UIKit
@MainActor
@Observable
final class GatewayConnectionController {
struct ManualAuthOverride: Equatable {
struct SetupAuth {
let token: String
let bootstrapToken: String
let password: String
var hasBootstrapToken: Bool {
!self.bootstrapToken.isEmpty
}
var shouldApplyTokenField: Bool {
!self.token.isEmpty || self.hasBootstrapToken
}
var shouldApplyPasswordField: Bool {
!self.password.isEmpty || self.hasBootstrapToken
}
var manualAuthOverride: ManualAuthOverride? {
ManualAuthOverride.normalized(
token: self.token,
bootstrapToken: self.bootstrapToken,
password: self.password)
}
}
let token: String?
let bootstrapToken: String?
let password: String?
static func explicit(
token: String?,
bootstrapToken: String?,
password: String?) -> ManualAuthOverride
{
let trimmedToken = token?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
let trimmedBootstrapToken = bootstrapToken?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
let trimmedPassword = password?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
return ManualAuthOverride(
token: trimmedToken.isEmpty ? nil : trimmedToken,
bootstrapToken: trimmedBootstrapToken.isEmpty ? nil : trimmedBootstrapToken,
password: trimmedPassword.isEmpty ? nil : trimmedPassword)
}
static func normalized(
token: String?,
bootstrapToken: String?,
password: String?) -> ManualAuthOverride?
{
let override = ManualAuthOverride.explicit(
token: token,
bootstrapToken: bootstrapToken,
password: password)
guard override.token != nil || override.bootstrapToken != nil || override.password != nil
else { return nil }
return override
}
static func currentManualInput(
token: String?,
pendingOverride: ManualAuthOverride?,
password: String?) -> ManualAuthOverride?
{
guard let pendingOverride else {
return ManualAuthOverride.normalized(token: token, bootstrapToken: nil, password: password)
}
return ManualAuthOverride.explicit(
token: token,
bootstrapToken: pendingOverride.bootstrapToken,
password: password)
}
static func setupAuth(from link: GatewayConnectDeepLink) -> SetupAuth {
SetupAuth(
token: link.token?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "",
bootstrapToken: link.bootstrapToken?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "",
password: link.password?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "")
}
}
private struct PendingTrustConnect {
let url: URL
let stableID: String
let isManual: Bool
let authOverride: ManualAuthOverride?
}
struct TrustPrompt: Identifiable, Equatable {
let stableID: String
let gatewayName: String
@@ -42,7 +129,7 @@ final class GatewayConnectionController {
private weak var appModel: NodeAppModel?
private var didAutoConnect = false
private var pendingServiceResolvers: [String: GatewayServiceResolver] = [:]
private var pendingTrustConnect: (url: URL, stableID: String, isManual: Bool)?
private var pendingTrustConnect: PendingTrustConnect?
init(appModel: NodeAppModel, startDiscovery: Bool = true) {
self.appModel = appModel
@@ -125,7 +212,11 @@ final class GatewayConnectionController {
guard let fp = await self.probeTLSFingerprint(url: url) else {
return "Failed to read TLS fingerprint from discovered gateway."
}
self.pendingTrustConnect = (url: url, stableID: stableID, isManual: false)
self.pendingTrustConnect = PendingTrustConnect(
url: url,
stableID: stableID,
isManual: false,
authOverride: nil)
self.pendingTrustPrompt = TrustPrompt(
stableID: stableID,
gatewayName: gateway.name,
@@ -162,12 +253,23 @@ final class GatewayConnectionController {
_ = await self.connectWithDiagnostics(gateway)
}
func connectManual(host: String, port: Int, useTLS: Bool) async {
let instanceId = UserDefaults.standard.string(forKey: "node.instanceId")?
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
let token = GatewaySettingsStore.loadGatewayToken(instanceId: instanceId)
let bootstrapToken = GatewaySettingsStore.loadGatewayBootstrapToken(instanceId: instanceId)
let password = GatewaySettingsStore.loadGatewayPassword(instanceId: instanceId)
func connectManual(
host: String,
port: Int,
useTLS: Bool,
authOverride: ManualAuthOverride? = nil) async
{
let instanceId = GatewaySettingsStore.currentInstanceID()
let token =
authOverride.map(\.token) ?? GatewaySettingsStore.loadGatewayToken(instanceId: instanceId)
let bootstrapToken =
authOverride.map(\.bootstrapToken) ?? GatewaySettingsStore.loadGatewayBootstrapToken(instanceId: instanceId)
let password =
authOverride.map(\.password) ?? GatewaySettingsStore.loadGatewayPassword(instanceId: instanceId)
let pendingAuthOverride = authOverride ?? ManualAuthOverride.normalized(
token: token,
bootstrapToken: bootstrapToken,
password: password)
let resolvedUseTLS = self.resolveManualUseTLS(host: host, useTLS: useTLS)
guard let resolvedPort = self.resolveManualPort(host: host, port: port, useTLS: resolvedUseTLS)
else { return }
@@ -181,7 +283,11 @@ final class GatewayConnectionController {
+ "Remote gateways must use HTTPS/WSS."
return
}
self.pendingTrustConnect = (url: url, stableID: stableID, isManual: true)
self.pendingTrustConnect = PendingTrustConnect(
url: url,
stableID: stableID,
isManual: true,
authOverride: pendingAuthOverride)
self.pendingTrustPrompt = TrustPrompt(
stableID: stableID,
gatewayName: "\(host):\(resolvedPort)",
@@ -269,11 +375,14 @@ final class GatewayConnectionController {
GatewaySettingsStore.saveLastGatewayConnectionDiscovered(stableID: pending.stableID, useTLS: true)
}
let instanceId = UserDefaults.standard.string(forKey: "node.instanceId")?
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
let token = GatewaySettingsStore.loadGatewayToken(instanceId: instanceId)
let bootstrapToken = GatewaySettingsStore.loadGatewayBootstrapToken(instanceId: instanceId)
let password = GatewaySettingsStore.loadGatewayPassword(instanceId: instanceId)
let instanceId = GatewaySettingsStore.currentInstanceID()
let token =
pending.authOverride.map(\.token) ?? GatewaySettingsStore.loadGatewayToken(instanceId: instanceId)
let bootstrapToken =
pending.authOverride.map(\.bootstrapToken) ?? GatewaySettingsStore.loadGatewayBootstrapToken(
instanceId: instanceId)
let password =
pending.authOverride.map(\.password) ?? GatewaySettingsStore.loadGatewayPassword(instanceId: instanceId)
let tlsParams = GatewayTLSParams(
required: true,
expectedFingerprint: prompt.fingerprintSha256,

View File

@@ -34,6 +34,17 @@ enum GatewaySettingsStore {
self.ensureLastDiscoveredGatewayStableID()
}
static func currentInstanceID(defaults: UserDefaults = .standard) -> String {
self.bootstrapPersistence()
if let value = defaults.string(forKey: self.instanceIdDefaultsKey)?
.trimmingCharacters(in: .whitespacesAndNewlines),
!value.isEmpty
{
return value
}
return self.loadStableInstanceID() ?? ""
}
static func loadStableInstanceID() -> String? {
if let value = KeychainStore.loadString(service: self.nodeService, account: self.instanceIdAccount)?
.trimmingCharacters(in: .whitespacesAndNewlines),
@@ -110,8 +121,15 @@ enum GatewaySettingsStore {
}
static func saveGatewayToken(_ token: String, instanceId: String) {
let trimmed = token.trimmingCharacters(in: .whitespacesAndNewlines)
if trimmed.isEmpty {
_ = KeychainStore.delete(
service: self.gatewayService,
account: self.gatewayTokenAccount(instanceId: instanceId))
return
}
_ = KeychainStore.saveString(
token,
trimmed,
service: self.gatewayService,
account: self.gatewayTokenAccount(instanceId: instanceId))
}
@@ -125,8 +143,13 @@ enum GatewaySettingsStore {
}
static func saveGatewayBootstrapToken(_ token: String, instanceId: String) {
let trimmed = token.trimmingCharacters(in: .whitespacesAndNewlines)
if trimmed.isEmpty {
self.clearGatewayBootstrapToken(instanceId: instanceId)
return
}
_ = KeychainStore.saveString(
token,
trimmed,
service: self.gatewayService,
account: self.gatewayBootstrapTokenAccount(instanceId: instanceId))
}
@@ -145,8 +168,15 @@ enum GatewaySettingsStore {
}
static func saveGatewayPassword(_ password: String, instanceId: String) {
let trimmed = password.trimmingCharacters(in: .whitespacesAndNewlines)
if trimmed.isEmpty {
_ = KeychainStore.delete(
service: self.gatewayService,
account: self.gatewayPasswordAccount(instanceId: instanceId))
return
}
_ = KeychainStore.saveString(
password,
trimmed,
service: self.gatewayService,
account: self.gatewayPasswordAccount(instanceId: instanceId))
}

View File

@@ -597,6 +597,18 @@ final class NodeAppModel {
}
}
func setTalkProviderSelection(_ rawValue: String) {
let selection = TalkModeProviderSelection.resolved(rawValue)
UserDefaults.standard.set(selection.rawValue, forKey: TalkModeProviderSelection.storageKey)
self.talkMode.applyProviderSelectionChanged()
}
func setTalkRealtimeVoiceSelection(_ rawValue: String) {
let voice = TalkModeRealtimeVoiceSelection.resolvedOverride(rawValue) ?? ""
UserDefaults.standard.set(voice, forKey: TalkModeRealtimeVoiceSelection.storageKey)
self.talkMode.applyProviderSelectionChanged()
}
func requestLocationPermissions(mode: OpenClawLocationMode) async -> Bool {
guard mode != .off else { return true }
let status = await self.locationService.ensureAuthorization(mode: mode)

View File

@@ -3,7 +3,7 @@ import OpenClawKit
enum GatewayOnboardingReset {
@MainActor
static func reset(
static func prepareForBootstrapPairing(
appModel: NodeAppModel,
instanceId: String,
defaults: UserDefaults = .standard)
@@ -15,10 +15,24 @@ enum GatewayOnboardingReset {
GatewaySettingsStore.deleteGatewayCredentials(instanceId: trimmedInstanceId)
}
GatewaySettingsStore.clearLastGatewayConnection()
GatewaySettingsStore.clearPreferredGatewayStableID()
GatewaySettingsStore.clearLastDiscoveredGatewayStableID()
let deviceId = DeviceIdentityStore.loadOrCreate().deviceId
DeviceAuthStore.clearToken(deviceId: deviceId, role: "node")
DeviceAuthStore.clearToken(deviceId: deviceId, role: "operator")
GatewaySettingsStore.clearLastGatewayConnection(defaults: defaults)
GatewaySettingsStore.clearPreferredGatewayStableID(defaults: defaults)
GatewaySettingsStore.clearLastDiscoveredGatewayStableID(defaults: defaults)
GatewayTLSStore.clearAllFingerprints()
defaults.set(false, forKey: "gateway.autoconnect")
}
@MainActor
static func reset(
appModel: NodeAppModel,
instanceId: String,
defaults: UserDefaults = .standard)
{
self.prepareForBootstrapPairing(appModel: appModel, instanceId: instanceId, defaults: defaults)
OnboardingStateStore.reset(defaults: defaults)
defaults.set(false, forKey: "gateway.onboardingComplete")

View File

@@ -109,6 +109,7 @@ private struct ManualEntryStep: View {
@State private var manualUseTLS: Bool = true
@State private var manualToken: String = ""
@State private var manualPassword: String = ""
@State private var pendingManualAuthOverride: GatewayConnectionController.ManualAuthOverride?
@State private var connectingGatewayID: String?
@State private var connectStatusText: String?
@@ -207,9 +208,8 @@ private struct ManualEntryStep: View {
defaults.set(self.manualPortValue() ?? 0, forKey: "gateway.manual.port")
defaults.set(self.manualUseTLS, forKey: "gateway.manual.tls")
if let instanceId = defaults.string(forKey: "node.instanceId")?.trimmingCharacters(in: .whitespacesAndNewlines),
!instanceId.isEmpty
{
let instanceId = GatewaySettingsStore.currentInstanceID()
if !instanceId.isEmpty {
let trimmedToken = self.manualToken.trimmingCharacters(in: .whitespacesAndNewlines)
let trimmedPassword = self.manualPassword.trimmingCharacters(in: .whitespacesAndNewlines)
if !trimmedToken.isEmpty {
@@ -220,10 +220,16 @@ private struct ManualEntryStep: View {
self.connectingGatewayID = "manual"
defer { self.connectingGatewayID = nil }
let authOverride = GatewayConnectionController.ManualAuthOverride.currentManualInput(
token: self.manualToken,
pendingOverride: self.pendingManualAuthOverride,
password: self.manualPassword)
self.pendingManualAuthOverride = nil
await self.gatewayController.connectManual(
host: host,
port: self.manualPortValue() ?? 0,
useTLS: self.manualUseTLS)
useTLS: self.manualUseTLS,
authOverride: authOverride)
}
private func manualPortValue() -> Int? {
@@ -258,24 +264,24 @@ private struct ManualEntryStep: View {
self.manualPortText = String(link.port)
self.manualUseTLS = link.tls
if let token = link.token, !token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
self.manualToken = token.trimmingCharacters(in: .whitespacesAndNewlines)
} else if link.bootstrapToken?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false {
self.manualToken = ""
let setupAuth = GatewayConnectionController.ManualAuthOverride.setupAuth(from: link)
if setupAuth.shouldApplyTokenField {
self.manualToken = setupAuth.token
}
if let password = link.password, !password.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
self.manualPassword = password.trimmingCharacters(in: .whitespacesAndNewlines)
} else if link.bootstrapToken?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false {
self.manualPassword = ""
if setupAuth.shouldApplyPasswordField {
self.manualPassword = setupAuth.password
}
let trimmedInstanceId = UserDefaults.standard.string(forKey: "node.instanceId")?
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
let trimmedInstanceId = GatewaySettingsStore.currentInstanceID()
if !trimmedInstanceId.isEmpty {
let trimmedBootstrapToken =
link.bootstrapToken?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
GatewaySettingsStore.saveGatewayBootstrapToken(trimmedBootstrapToken, instanceId: trimmedInstanceId)
if setupAuth.hasBootstrapToken {
GatewayOnboardingReset.prepareForBootstrapPairing(
appModel: self.appModel,
instanceId: trimmedInstanceId)
}
GatewaySettingsStore.saveGatewayBootstrapToken(setupAuth.bootstrapToken, instanceId: trimmedInstanceId)
}
self.pendingManualAuthOverride = setupAuth.manualAuthOverride
self.setupStatusText = "Setup code applied."
}

View File

@@ -71,6 +71,7 @@ struct OnboardingWizardView: View {
@State private var selectedPhoto: PhotosPickerItem?
@State private var showGatewayProblemDetails: Bool = false
@State private var lastPairingAutoResumeAttemptAt: Date?
@State private var pendingManualAuthOverride: GatewayConnectionController.ManualAuthOverride?
private static let pairingAutoResumeTicker = Timer.publish(every: 2.0, on: .main, in: .common).autoconnect()
let allowSkip: Bool
@@ -744,18 +745,20 @@ struct OnboardingWizardView: View {
self.manualHost = link.host
self.manualPort = link.port
self.manualTLS = link.tls
let trimmedBootstrapToken = link.bootstrapToken?.trimmingCharacters(in: .whitespacesAndNewlines)
self.saveGatewayBootstrapToken(trimmedBootstrapToken)
if let token = link.token?.trimmingCharacters(in: .whitespacesAndNewlines), !token.isEmpty {
self.gatewayToken = token
} else if trimmedBootstrapToken?.isEmpty == false {
self.gatewayToken = ""
let setupAuth = GatewayConnectionController.ManualAuthOverride.setupAuth(from: link)
if setupAuth.hasBootstrapToken {
GatewayOnboardingReset.prepareForBootstrapPairing(
appModel: self.appModel,
instanceId: GatewaySettingsStore.currentInstanceID())
}
if let password = link.password?.trimmingCharacters(in: .whitespacesAndNewlines), !password.isEmpty {
self.gatewayPassword = password
} else if trimmedBootstrapToken?.isEmpty == false {
self.gatewayPassword = ""
self.saveGatewayBootstrapToken(setupAuth.bootstrapToken)
if setupAuth.shouldApplyTokenField {
self.gatewayToken = setupAuth.token
}
if setupAuth.shouldApplyPasswordField {
self.gatewayPassword = setupAuth.password
}
self.pendingManualAuthOverride = setupAuth.manualAuthOverride
self.saveGatewayCredentials(token: self.gatewayToken, password: self.gatewayPassword)
self.showQRScanner = false
self.connectMessage = "Connecting via QR code…"
@@ -937,7 +940,7 @@ struct OnboardingWizardView: View {
}
private func saveGatewayCredentials(token: String, password: String) {
let trimmedInstanceId = self.instanceId.trimmingCharacters(in: .whitespacesAndNewlines)
let trimmedInstanceId = GatewaySettingsStore.currentInstanceID()
guard !trimmedInstanceId.isEmpty else { return }
let trimmedToken = token.trimmingCharacters(in: .whitespacesAndNewlines)
GatewaySettingsStore.saveGatewayToken(trimmedToken, instanceId: trimmedInstanceId)
@@ -946,7 +949,7 @@ struct OnboardingWizardView: View {
}
private func saveGatewayBootstrapToken(_ token: String?) {
let trimmedInstanceId = self.instanceId.trimmingCharacters(in: .whitespacesAndNewlines)
let trimmedInstanceId = GatewaySettingsStore.currentInstanceID()
guard !trimmedInstanceId.isEmpty else { return }
let trimmedToken = token?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
GatewaySettingsStore.saveGatewayBootstrapToken(trimmedToken, instanceId: trimmedInstanceId)
@@ -1001,7 +1004,16 @@ struct OnboardingWizardView: View {
self.connectMessage = "Connecting to \(host)"
self.statusLine = "Connecting to \(host):\(self.manualPort)"
defer { self.connectingGatewayID = nil }
await self.gatewayController.connectManual(host: host, port: self.manualPort, useTLS: self.manualTLS)
let authOverride = GatewayConnectionController.ManualAuthOverride.currentManualInput(
token: self.gatewayToken,
pendingOverride: self.pendingManualAuthOverride,
password: self.gatewayPassword)
self.pendingManualAuthOverride = nil
await self.gatewayController.connectManual(
host: host,
port: self.manualPort,
useTLS: self.manualTLS,
authOverride: authOverride)
}
private func retryLastAttempt(silent: Bool = false) async {

View File

@@ -21,6 +21,9 @@ struct SettingsTab: View {
@AppStorage("node.instanceId") private var instanceId: String = UUID().uuidString
@AppStorage("voiceWake.enabled") private var voiceWakeEnabled: Bool = false
@AppStorage("talk.enabled") private var talkEnabled: Bool = false
@AppStorage(TalkModeProviderSelection.storageKey) private var talkProviderSelectionRaw: String =
TalkModeProviderSelection.gatewayDefault.rawValue
@AppStorage(TalkModeRealtimeVoiceSelection.storageKey) private var talkRealtimeVoiceSelectionRaw: String = ""
@AppStorage(TalkSpeechLocale.storageKey) private var talkSpeechLocale: String = TalkSpeechLocale.automaticID
@AppStorage("talk.button.enabled") private var talkButtonEnabled: Bool = true
@AppStorage("talk.background.enabled") private var talkBackgroundEnabled: Bool = false
@@ -54,6 +57,7 @@ struct SettingsTab: View {
@State private var manualGatewayPortText: String = ""
@State private var gatewayExpanded: Bool = true
@State private var selectedAgentPickerId: String = ""
@State private var pendingManualAuthOverride: GatewayConnectionController.ManualAuthOverride?
@State private var showResetOnboardingAlert: Bool = false
@State private var showGatewayProblemDetails: Bool = false
@@ -344,64 +348,7 @@ struct SettingsTab: View {
help: "Keeps the screen awake while OpenClaw is open.")
DisclosureGroup("Advanced") {
VStack(alignment: .leading, spacing: 8) {
Text("Talk Voice (Gateway)")
.font(.footnote.weight(.semibold))
.foregroundStyle(.secondary)
LabeledContent("Provider", value: "ElevenLabs")
LabeledContent(
"API Key",
value: self.appModel.talkMode.gatewayTalkConfigLoaded
? (
self.appModel.talkMode.gatewayTalkApiKeyConfigured
? "Configured"
: "Not configured")
: "Not loaded")
LabeledContent(
"Default Model",
value: self.appModel.talkMode.gatewayTalkDefaultModelId ?? "eleven_v3 (fallback)")
LabeledContent(
"Default Voice",
value: self.appModel.talkMode.gatewayTalkDefaultVoiceId ?? "auto (first available)")
Text("Configured on gateway via talk.apiKey, talk.modelId, and talk.voiceId.")
.font(.footnote)
.foregroundStyle(.secondary)
}
self.featureToggle(
"Show Talk Control",
isOn: self.$talkButtonEnabled,
help: "Shows the Talk control in the main toolbar.")
TextField("Default Share Instruction", text: self.$defaultShareInstruction, axis: .vertical)
.lineLimit(2...6)
.textInputAutocapitalization(.sentences)
HStack(spacing: 8) {
Text("Default Share Instruction")
.font(.footnote)
.foregroundStyle(.secondary)
Spacer()
Button {
self.activeFeatureHelp = FeatureHelp(
title: "Default Share Instruction",
message: "Appends this instruction when sharing content "
+ "into OpenClaw from iOS.")
} label: {
Image(systemName: "info.circle")
.foregroundStyle(.secondary)
}
.buttonStyle(.plain)
.accessibilityLabel("Default Share Instruction info")
}
VStack(alignment: .leading, spacing: 8) {
Button {
Task { await self.appModel.runSharePipelineSelfTest() }
} label: {
Label("Run Share Self-Test", systemImage: "checkmark.seal")
}
Text(self.appModel.lastShareEventText)
.font(.footnote)
.foregroundStyle(.secondary)
}
self.advancedAppSettingsView()
}
}
@@ -662,6 +609,120 @@ struct SettingsTab: View {
return trimmed.isEmpty ? "Not connected" : trimmed
}
private var shouldShowRealtimeVoicePicker: Bool {
let providerSelection = TalkModeProviderSelection.resolved(self.talkProviderSelectionRaw)
return providerSelection == .openAIRealtime
|| self.appModel.talkMode.gatewayTalkUsesRealtimeRelay
}
private func talkVoiceSettingsView() -> AnyView {
AnyView(VStack(alignment: .leading, spacing: 8) {
Text("Talk Voice (Gateway)")
.font(.footnote.weight(.semibold))
.foregroundStyle(.secondary)
Picker("Provider", selection: self.talkProviderSelectionBinding) {
ForEach(TalkModeProviderSelection.allCases) { option in
Text(option.label).tag(option.rawValue)
}
}
if self.shouldShowRealtimeVoicePicker {
Picker("Realtime Voice", selection: self.talkRealtimeVoiceSelectionBinding) {
Text("Gateway Default").tag("")
ForEach(TalkModeRealtimeVoiceSelection.voices, id: \.self) { voice in
Text(TalkModeRealtimeVoiceSelection.label(for: voice)).tag(voice)
}
}
}
LabeledContent(
"Active Provider",
value: self.appModel.talkMode.gatewayTalkProviderLabel)
LabeledContent(
"Transport",
value: self.appModel.talkMode.gatewayTalkTransportLabel)
LabeledContent(
"API Key",
value: self.appModel.talkMode.gatewayTalkConfigLoaded
? (
self.appModel.talkMode.gatewayTalkApiKeyConfigured
? "Configured"
: "Not configured")
: "Not loaded")
LabeledContent(
"Default Model",
value: self.appModel.talkMode.gatewayTalkDefaultModelId ?? "eleven_v3 (fallback)")
LabeledContent(
"Default Voice",
value: self.appModel.talkMode.gatewayTalkDefaultVoiceId ?? "auto (first available)")
if let realtimeProvider = self.appModel.talkMode.gatewayTalkRealtimeProviderLabel {
LabeledContent("Realtime Provider", value: realtimeProvider)
}
Text("Realtime uses gateway auth via OpenAI API key or OAuth.")
.font(.footnote)
.foregroundStyle(.secondary)
})
}
private var talkProviderSelectionBinding: Binding<String> {
Binding(
get: { self.talkProviderSelectionRaw },
set: { newValue in
let selection = TalkModeProviderSelection.resolved(newValue)
self.talkProviderSelectionRaw = selection.rawValue
self.appModel.setTalkProviderSelection(selection.rawValue)
})
}
private var talkRealtimeVoiceSelectionBinding: Binding<String> {
Binding(
get: { self.talkRealtimeVoiceSelectionRaw },
set: { newValue in
let voice = TalkModeRealtimeVoiceSelection.resolvedOverride(newValue) ?? ""
self.talkRealtimeVoiceSelectionRaw = voice
self.appModel.setTalkRealtimeVoiceSelection(voice)
})
}
private func advancedAppSettingsView() -> AnyView {
AnyView(Group {
self.talkVoiceSettingsView()
self.featureToggle(
"Show Talk Control",
isOn: self.$talkButtonEnabled,
help: "Shows the Talk control in the main toolbar.")
TextField("Default Share Instruction", text: self.$defaultShareInstruction, axis: .vertical)
.lineLimit(2...6)
.textInputAutocapitalization(.sentences)
HStack(spacing: 8) {
Text("Default Share Instruction")
.font(.footnote)
.foregroundStyle(.secondary)
Spacer()
Button {
self.activeFeatureHelp = FeatureHelp(
title: "Default Share Instruction",
message: "Appends this instruction when sharing content "
+ "into OpenClaw from iOS.")
} label: {
Image(systemName: "info.circle")
.foregroundStyle(.secondary)
}
.buttonStyle(.plain)
.accessibilityLabel("Default Share Instruction info")
}
VStack(alignment: .leading, spacing: 8) {
Button {
Task { await self.appModel.runSharePipelineSelfTest() }
} label: {
Label("Run Share Self-Test", systemImage: "checkmark.seal")
}
Text(self.appModel.lastShareEventText)
.font(.footnote)
.foregroundStyle(.secondary)
}
})
}
private func featureToggle(
_ title: String,
isOn: Binding<Bool>,
@@ -819,36 +880,29 @@ struct SettingsTab: View {
self.manualGatewayPortText = String(link.port)
self.manualGatewayTLS = link.tls
let trimmedInstanceId = self.instanceId.trimmingCharacters(in: .whitespacesAndNewlines)
let trimmedBootstrapToken =
link.bootstrapToken?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
let trimmedInstanceId = GatewaySettingsStore.currentInstanceID()
let setupAuth = GatewayConnectionController.ManualAuthOverride.setupAuth(from: link)
if setupAuth.hasBootstrapToken {
GatewayOnboardingReset.prepareForBootstrapPairing(
appModel: self.appModel,
instanceId: trimmedInstanceId)
}
if !trimmedInstanceId.isEmpty {
GatewaySettingsStore.saveGatewayBootstrapToken(trimmedBootstrapToken, instanceId: trimmedInstanceId)
GatewaySettingsStore.saveGatewayBootstrapToken(setupAuth.bootstrapToken, instanceId: trimmedInstanceId)
}
if let token = link.token, !token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
let trimmedToken = token.trimmingCharacters(in: .whitespacesAndNewlines)
self.gatewayToken = trimmedToken
if setupAuth.shouldApplyTokenField {
self.gatewayToken = setupAuth.token
if !trimmedInstanceId.isEmpty {
GatewaySettingsStore.saveGatewayToken(trimmedToken, instanceId: trimmedInstanceId)
}
} else if !trimmedBootstrapToken.isEmpty {
self.gatewayToken = ""
if !trimmedInstanceId.isEmpty {
GatewaySettingsStore.saveGatewayToken("", instanceId: trimmedInstanceId)
GatewaySettingsStore.saveGatewayToken(setupAuth.token, instanceId: trimmedInstanceId)
}
}
if let password = link.password, !password.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
let trimmedPassword = password.trimmingCharacters(in: .whitespacesAndNewlines)
self.gatewayPassword = trimmedPassword
if setupAuth.shouldApplyPasswordField {
self.gatewayPassword = setupAuth.password
if !trimmedInstanceId.isEmpty {
GatewaySettingsStore.saveGatewayPassword(trimmedPassword, instanceId: trimmedInstanceId)
}
} else if !trimmedBootstrapToken.isEmpty {
self.gatewayPassword = ""
if !trimmedInstanceId.isEmpty {
GatewaySettingsStore.saveGatewayPassword("", instanceId: trimmedInstanceId)
GatewaySettingsStore.saveGatewayPassword(setupAuth.password, instanceId: trimmedInstanceId)
}
}
self.pendingManualAuthOverride = setupAuth.manualAuthOverride
}
private func openGatewayQRScanner() {
@@ -940,10 +994,16 @@ struct SettingsTab: View {
GatewayDiagnostics.log(
"connect manual host=\(host) port=\(self.manualGatewayPort) tls=\(self.manualGatewayTLS)")
let authOverride = GatewayConnectionController.ManualAuthOverride.currentManualInput(
token: self.gatewayToken,
pendingOverride: self.pendingManualAuthOverride,
password: self.gatewayPassword)
self.pendingManualAuthOverride = nil
await self.gatewayController.connectManual(
host: host,
port: self.manualGatewayPort,
useTLS: self.manualGatewayTLS)
useTLS: self.manualGatewayTLS,
authOverride: authOverride)
}
private var setupStatusLine: String? {

View File

@@ -0,0 +1,588 @@
import AVFAudio
import Foundation
import OpenClawChatUI
import OpenClawKit
import OpenClawProtocol
import OSLog
private func makeRealtimeAudioTapBlock(
inputSampleRate: Double,
targetSampleRate: Double,
onAudio: @escaping (Data, Double) -> Void) -> AVAudioNodeTapBlock
{
{ buffer, _ in
// This callback runs on Core Audio's realtime queue, not MainActor.
let encoded = RealtimeTalkRelaySession.encodePCM16(
buffer: buffer,
inputSampleRate: inputSampleRate,
targetSampleRate: targetSampleRate)
guard !encoded.isEmpty else { return }
let timestampMs = ProcessInfo.processInfo.systemUptime * 1000
onAudio(encoded, timestampMs)
}
}
private actor RealtimeAudioSender {
private let gateway: GatewayNodeSession
private var relaySessionId: String?
private var pendingSends = 0
private let maxPendingSends = 4
init(gateway: GatewayNodeSession, relaySessionId: String) {
self.gateway = gateway
self.relaySessionId = relaySessionId
}
func close() {
self.relaySessionId = nil
}
func send(_ data: Data, timestampMs: Double) async -> String? {
guard let relaySessionId else { return nil }
guard self.pendingSends < self.maxPendingSends else { return nil }
self.pendingSends += 1
defer { self.pendingSends -= 1 }
let payload: [String: Any] = [
"sessionId": relaySessionId,
"audioBase64": data.base64EncodedString(),
"timestamp": timestampMs,
]
do {
_ = try await Self.requestJSON(
gateway: self.gateway,
method: "talk.session.appendAudio",
payload: payload,
decodeAs: TalkSessionOkResult.self,
timeoutSeconds: 8)
return nil
} catch {
return error.localizedDescription
}
}
private static func requestJSON<T: Decodable>(
gateway: GatewayNodeSession,
method: String,
payload: [String: Any],
decodeAs type: T.Type,
timeoutSeconds: Int) async throws -> T
{
let data = try JSONSerialization.data(withJSONObject: payload)
guard let json = String(data: data, encoding: .utf8) else {
throw NSError(domain: "RealtimeTalkRelay", code: 4, userInfo: [
NSLocalizedDescriptionKey: "Failed to encode \(method) payload",
])
}
let response = try await gateway.request(
method: method,
paramsJSON: json,
timeoutSeconds: timeoutSeconds)
return try JSONDecoder().decode(type, from: response)
}
}
@MainActor
final class RealtimeTalkRelaySession {
private static let agentControlToolName = "openclaw_agent_control"
struct Options {
let sessionKey: String
let provider: String?
let model: String?
let voice: String?
}
private struct ToolCallStartResponse: Decodable {
let runId: String?
let idempotencyKey: String?
}
private struct ChatCompletionResult {
let text: String?
let failed: Bool
}
private nonisolated static let expectedInputEncoding = "pcm16"
private nonisolated static let expectedOutputEncoding = "pcm16"
private nonisolated static let defaultSampleRateHz = 24000
private nonisolated static let audioFrameBufferSize: AVAudioFrameCount = 2048
private let gateway: GatewayNodeSession
private let options: Options
private let pcmPlayer: PCMStreamingAudioPlaying
private let logger = Logger(subsystem: "ai.openclaw", category: "RealtimeTalkRelay")
private let onStatus: (String) -> Void
private let onSpeakingChanged: (Bool) -> Void
private let audioEngine = AVAudioEngine()
private var relaySessionId: String?
private var inputSampleRateHz = Double(RealtimeTalkRelaySession.defaultSampleRateHz)
private var outputSampleRateHz = Double(RealtimeTalkRelaySession.defaultSampleRateHz)
private var eventTask: Task<Void, Never>?
private var outputTask: Task<Void, Never>?
private var outputContinuation: AsyncThrowingStream<Data, Error>.Continuation?
private var audioSender: RealtimeAudioSender?
private var isClosed = false
private var isOutputPlaying = false
init(
gateway: GatewayNodeSession,
options: Options,
pcmPlayer: PCMStreamingAudioPlaying,
onStatus: @escaping (String) -> Void,
onSpeakingChanged: @escaping (Bool) -> Void)
{
self.gateway = gateway
self.options = options
self.pcmPlayer = pcmPlayer
self.onStatus = onStatus
self.onSpeakingChanged = onSpeakingChanged
}
func start() async throws {
self.isClosed = false
self.onStatus("Connecting realtime…")
let result = try await self.createRelaySession()
guard let relaySessionId = result.relaysessionid?.trimmingCharacters(in: .whitespacesAndNewlines),
!relaySessionId.isEmpty
else {
throw NSError(domain: "RealtimeTalkRelay", code: 1, userInfo: [
NSLocalizedDescriptionKey: "Gateway did not return a realtime relay session",
])
}
self.relaySessionId = relaySessionId
do {
self.audioSender = RealtimeAudioSender(gateway: self.gateway, relaySessionId: relaySessionId)
let eventStream = await self.gateway.subscribeServerEvents(bufferingNewest: 200)
self.startEventPump(stream: eventStream)
self.configureAudioContract(result.audio)
self.startOutputPlayback()
try self.startMicrophonePump()
self.onStatus("Listening (Realtime)")
} catch {
let createdRelaySessionId = self.relaySessionId
self.close(sendClose: false)
if let createdRelaySessionId {
await Self.closeRelaySession(gateway: self.gateway, relaySessionId: createdRelaySessionId)
}
throw error
}
}
func stop() {
self.close(sendClose: true)
}
private func close(sendClose: Bool) {
guard !self.isClosed else { return }
self.isClosed = true
self.stopMicrophonePump()
self.eventTask?.cancel()
self.eventTask = nil
let audioSender = self.audioSender
self.audioSender = nil
Task { await audioSender?.close() }
self.stopOutputPlayback()
if sendClose, let relaySessionId = self.relaySessionId {
Task { [gateway] in
await Self.closeRelaySession(gateway: gateway, relaySessionId: relaySessionId)
}
}
self.relaySessionId = nil
self.onSpeakingChanged(false)
}
private nonisolated static func closeRelaySession(
gateway: GatewayNodeSession,
relaySessionId: String) async
{
let payload = ["sessionId": relaySessionId]
let data = try? JSONSerialization.data(withJSONObject: payload)
let json = data.flatMap { String(data: $0, encoding: .utf8) }
_ = try? await gateway.request(
method: "talk.session.close",
paramsJSON: json,
timeoutSeconds: 8)
}
func cancelOutput(reason: String = "user") {
self.stopOutputPlayback()
self.startOutputPlayback()
guard let relaySessionId else { return }
Task { [gateway] in
let payload: [String: Any] = [
"sessionId": relaySessionId,
"reason": reason,
]
let data = try? JSONSerialization.data(withJSONObject: payload)
let json = data.flatMap { String(data: $0, encoding: .utf8) }
_ = try? await gateway.request(
method: "talk.session.cancelOutput",
paramsJSON: json,
timeoutSeconds: 8)
}
}
private func createRelaySession() async throws -> TalkSessionCreateResult {
var payload: [String: Any] = [
"sessionKey": self.options.sessionKey,
"mode": "realtime",
"transport": "gateway-relay",
"brain": "agent-consult",
]
if let provider = self.nonEmpty(self.options.provider) {
payload["provider"] = provider
}
if let model = self.nonEmpty(self.options.model) {
payload["model"] = model
}
if let voice = self.nonEmpty(self.options.voice) {
payload["voice"] = voice
}
let data = try JSONSerialization.data(withJSONObject: payload)
guard let json = String(data: data, encoding: .utf8) else {
throw NSError(domain: "RealtimeTalkRelay", code: 2, userInfo: [
NSLocalizedDescriptionKey: "Failed to encode realtime relay request",
])
}
let response = try await self.gateway.request(
method: "talk.session.create",
paramsJSON: json,
timeoutSeconds: 20)
return try JSONDecoder().decode(TalkSessionCreateResult.self, from: response)
}
private func configureAudioContract(_ raw: AnyCodable?) {
guard let audio = raw?.dictionaryValue else { return }
let inputEncoding = audio["inputEncoding"]?.stringValue ?? Self.expectedInputEncoding
let outputEncoding = audio["outputEncoding"]?.stringValue ?? Self.expectedOutputEncoding
if inputEncoding != Self.expectedInputEncoding || outputEncoding != Self.expectedOutputEncoding {
let message = "unexpected realtime relay audio contract input=\(inputEncoding) output=\(outputEncoding)"
self.logger.warning("\(message, privacy: .public)")
}
self.inputSampleRateHz = audio["inputSampleRateHz"]?.doubleValue
?? Double(Self.defaultSampleRateHz)
self.outputSampleRateHz = audio["outputSampleRateHz"]?.doubleValue
?? Double(Self.defaultSampleRateHz)
}
private func startEventPump(stream: AsyncStream<EventFrame>) {
self.eventTask?.cancel()
self.eventTask = Task { [weak self] in
for await event in stream {
if Task.isCancelled { return }
await self?.handleGatewayEvent(event)
}
}
}
private func handleGatewayEvent(_ event: EventFrame) async {
guard event.event == "talk.event",
let payload = event.payload?.dictionaryValue
else { return }
if let relaySessionId,
payload["relaySessionId"]?.stringValue != relaySessionId
{
return
}
guard let type = payload["type"]?.stringValue else { return }
switch type {
case "ready":
self.onStatus("Listening (Realtime)")
case "audio":
guard let base64 = payload["audioBase64"]?.stringValue,
let data = Data(base64Encoded: base64)
else { return }
self.isOutputPlaying = true
self.onSpeakingChanged(true)
self.outputContinuation?.yield(data)
case "clear":
self.stopOutputPlayback()
self.startOutputPlayback()
case "transcript":
self.handleTranscriptEvent(payload)
case "toolCall":
await self.handleToolCall(payload)
case "error":
let message = payload["message"]?.stringValue ?? "Realtime failed"
GatewayDiagnostics.log("talk realtime: error=\(Self.safeLogMessage(message))")
self.onStatus(message)
case "close":
self.onStatus("Ready")
self.close(sendClose: false)
default:
return
}
}
private func handleTranscriptEvent(_ payload: [String: AnyCodable]) {
guard payload["final"]?.boolValue == true else { return }
let role = payload["role"]?.stringValue ?? ""
if role == "user" {
self.onStatus("Thinking…")
} else if role == "assistant" {
self.onStatus("Listening (Realtime)")
}
}
private func handleToolCall(_ payload: [String: AnyCodable]) async {
guard let relaySessionId,
let callId = payload["callId"]?.stringValue,
let name = payload["name"]?.stringValue
else { return }
self.onStatus("Thinking…")
do {
if name == Self.agentControlToolName {
try await self.handleAgentControlToolCall(
callId: callId,
relaySessionId: relaySessionId,
args: payload["args"])
return
}
let completionStream = await self.gateway.subscribeServerEvents(bufferingNewest: 200)
let args = payload["args"]?.foundationValue ?? [:]
let startPayload: [String: Any] = [
"sessionKey": self.options.sessionKey,
"callId": callId,
"name": name,
"args": args,
"relaySessionId": relaySessionId,
]
let startResponse = try await self.requestJSON(
method: "talk.client.toolCall",
payload: startPayload,
decodeAs: ToolCallStartResponse.self,
timeoutSeconds: 30)
guard let runId = startResponse.runId ?? startResponse.idempotencyKey else {
throw NSError(domain: "RealtimeTalkRelay", code: 3, userInfo: [
NSLocalizedDescriptionKey: "Realtime tool call did not return a run id",
])
}
let completion = await self.waitForChatCompletion(
runId: runId,
stream: completionStream,
timeoutSeconds: 120)
let result: [String: Any] = completion.failed
? ["error": "OpenClaw tool call failed"]
: ["text": completion.text ?? "OpenClaw finished with no text."]
try await self.submitToolResult(callId: callId, result: result)
self.onStatus("Listening (Realtime)")
} catch {
try? await self.submitToolResult(callId: callId, result: [
"error": error.localizedDescription,
])
self.onStatus("Listening (Realtime)")
}
}
private func handleAgentControlToolCall(
callId: String,
relaySessionId: String,
args: AnyCodable?) async throws
{
let controlArgs = args?.dictionaryValue ?? [:]
var payload: [String: Any] = [
"sessionId": relaySessionId,
"sessionKey": self.options.sessionKey,
"text": controlArgs["text"]?.stringValue?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "status",
]
if let mode = controlArgs["mode"]?.stringValue?.trimmingCharacters(in: .whitespacesAndNewlines),
!mode.isEmpty
{
payload["mode"] = mode
}
let response = try await self.requestJSON(
method: "talk.session.steer",
payload: payload,
decodeAs: AnyCodable.self,
timeoutSeconds: 30)
let result = response.dictionaryValue?.mapValues(\.foundationValue) ?? [
"result": response.foundationValue,
]
try await self.submitToolResult(callId: callId, result: result)
self.onStatus("Listening (Realtime)")
}
private func submitToolResult(callId: String, result: [String: Any]) async throws {
guard let relaySessionId else { return }
let payload: [String: Any] = [
"sessionId": relaySessionId,
"callId": callId,
"result": result,
]
_ = try await self.requestJSON(
method: "talk.session.submitToolResult",
payload: payload,
decodeAs: TalkSessionOkResult.self,
timeoutSeconds: 30)
}
private func waitForChatCompletion(
runId: String,
stream: AsyncStream<EventFrame>,
timeoutSeconds: Int) async -> ChatCompletionResult
{
await withTaskGroup(of: ChatCompletionResult.self) { group in
group.addTask {
for await event in stream {
if Task.isCancelled {
return ChatCompletionResult(text: nil, failed: true)
}
guard event.event == "chat",
let payload = event.payload,
let chatEvent = try? GatewayPayloadDecoding.decode(
payload,
as: OpenClawChatEventPayload.self),
chatEvent.runId == runId
else { continue }
if chatEvent.state == "final" {
return ChatCompletionResult(
text: OpenClawChatEventText.assistantText(from: chatEvent),
failed: false)
}
if chatEvent.state == "aborted" || chatEvent.state == "error" {
return ChatCompletionResult(text: nil, failed: true)
}
}
return ChatCompletionResult(text: nil, failed: true)
}
group.addTask {
try? await Task.sleep(nanoseconds: UInt64(timeoutSeconds) * 1_000_000_000)
return ChatCompletionResult(text: nil, failed: true)
}
let result = await group.next() ?? ChatCompletionResult(text: nil, failed: true)
group.cancelAll()
return result
}
}
private func requestJSON<T: Decodable>(
method: String,
payload: [String: Any],
decodeAs type: T.Type,
timeoutSeconds: Int) async throws -> T
{
let data = try JSONSerialization.data(withJSONObject: payload)
guard let json = String(data: data, encoding: .utf8) else {
throw NSError(domain: "RealtimeTalkRelay", code: 4, userInfo: [
NSLocalizedDescriptionKey: "Failed to encode \(method) payload",
])
}
let response = try await self.gateway.request(
method: method,
paramsJSON: json,
timeoutSeconds: timeoutSeconds)
return try JSONDecoder().decode(type, from: response)
}
private func startMicrophonePump() throws {
self.stopMicrophonePump()
let input = self.audioEngine.inputNode
let format = input.inputFormat(forBus: 0)
let targetSampleRate = self.inputSampleRateHz
guard format.sampleRate > 0, format.channelCount > 0 else {
throw NSError(domain: "RealtimeTalkRelay", code: 5, userInfo: [
NSLocalizedDescriptionKey: "Invalid realtime audio input format",
])
}
let tapBlock = makeRealtimeAudioTapBlock(
inputSampleRate: format.sampleRate,
targetSampleRate: targetSampleRate)
{ [weak self, audioSender = self.audioSender] encoded, timestampMs in
guard let audioSender else { return }
Task {
guard let message = await audioSender.send(encoded, timestampMs: timestampMs) else { return }
await MainActor.run { [weak self] in
guard let self, !self.isClosed else { return }
self.onStatus("Realtime audio failed: \(message)")
}
}
}
input.installTap(
onBus: 0,
bufferSize: Self.audioFrameBufferSize,
format: format,
block: tapBlock)
self.audioEngine.prepare()
try self.audioEngine.start()
}
private func stopMicrophonePump() {
self.audioEngine.inputNode.removeTap(onBus: 0)
self.audioEngine.stop()
}
private func startOutputPlayback() {
self.stopOutputPlayback()
let stream = AsyncThrowingStream<Data, Error> { continuation in
self.outputContinuation = continuation
}
self.outputTask = Task { [weak self] in
guard let self else { return }
let result = await self.pcmPlayer.play(stream: stream, sampleRate: self.outputSampleRateHz)
await MainActor.run {
if !result.finished, let interruptedAt = result.interruptedAt {
self.logger.info("realtime output interrupted at \(interruptedAt, privacy: .public)s")
}
self.isOutputPlaying = false
self.onSpeakingChanged(false)
}
}
}
private func stopOutputPlayback() {
self.outputContinuation?.finish()
self.outputContinuation = nil
self.outputTask?.cancel()
self.outputTask = nil
_ = self.pcmPlayer.stop()
self.isOutputPlaying = false
self.onSpeakingChanged(false)
}
fileprivate nonisolated static func encodePCM16(
buffer: AVAudioPCMBuffer,
inputSampleRate: Double,
targetSampleRate: Double) -> Data
{
guard let channelData = buffer.floatChannelData,
buffer.frameLength > 0,
inputSampleRate > 0,
targetSampleRate > 0
else { return Data() }
let frameCount = Int(buffer.frameLength)
let channelCount = max(1, Int(buffer.format.channelCount))
let outputCount = max(1, Int((Double(frameCount) * targetSampleRate / inputSampleRate).rounded(.down)))
var data = Data(capacity: outputCount * MemoryLayout<Int16>.size)
for index in 0..<outputCount {
let sourcePosition = Double(index) * inputSampleRate / targetSampleRate
let lower = min(frameCount - 1, Int(sourcePosition.rounded(.down)))
let upper = min(frameCount - 1, lower + 1)
let fraction = Float(sourcePosition - Double(lower))
var mixed: Float = 0
for channel in 0..<channelCount {
let samples = channelData[channel]
mixed += samples[lower] + ((samples[upper] - samples[lower]) * fraction)
}
let sample = max(-1, min(1, mixed / Float(channelCount)))
var intSample = Int16((sample * Float(Int16.max)).rounded()).littleEndian
withUnsafeBytes(of: &intSample) { data.append(contentsOf: $0) }
}
return data
}
private nonisolated static func safeLogMessage(_ value: String) -> String {
let singleLine = value
.replacingOccurrences(of: "\n", with: " ")
.replacingOccurrences(of: "\r", with: " ")
if singleLine.count <= 180 {
return singleLine
}
return String(singleLine.prefix(180)) + "..."
}
private func nonEmpty(_ value: String?) -> String? {
let trimmed = value?.trimmingCharacters(in: .whitespacesAndNewlines)
return trimmed?.isEmpty == false ? trimmed : nil
}
}

View File

@@ -1,14 +1,77 @@
import Foundation
import OpenClawKit
enum TalkModeExecutionMode {
case native
case realtimeRelay
}
enum TalkModeProviderSelection: String, CaseIterable, Identifiable {
case gatewayDefault = "gateway"
case nativeElevenLabs = "elevenlabs"
case openAIRealtime = "openai-realtime"
static let storageKey = "talk.providerSelection"
var id: String {
self.rawValue
}
var label: String {
switch self {
case .gatewayDefault:
"Gateway Default"
case .nativeElevenLabs:
"ElevenLabs"
case .openAIRealtime:
"Realtime-2 (OpenAI)"
}
}
static func resolved(_ raw: String?) -> TalkModeProviderSelection {
let trimmed = (raw ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
return TalkModeProviderSelection(rawValue: trimmed) ?? .gatewayDefault
}
}
enum TalkModeRealtimeVoiceSelection {
static let storageKey = "talk.realtime.voiceSelection"
static let voices = [
"alloy",
"ash",
"ballad",
"coral",
"echo",
"sage",
"shimmer",
"verse",
"marin",
"cedar",
]
static func resolvedOverride(_ raw: String?) -> String? {
let trimmed = (raw ?? "").trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
guard !trimmed.isEmpty else { return nil }
return Self.voices.contains(trimmed) ? trimmed : nil
}
static func label(for voice: String) -> String {
voice.prefix(1).uppercased() + String(voice.dropFirst())
}
}
struct TalkModeGatewayConfigState {
let activeProvider: String
let normalizedPayload: Bool
let missingResolvedPayload: Bool
let executionMode: TalkModeExecutionMode
let defaultVoiceId: String?
let voiceAliases: [String: String]
let defaultModelId: String
let defaultOutputFormat: String?
let realtimeProvider: String?
let realtimeModelId: String?
let realtimeVoiceId: String?
let rawConfigApiKey: String?
let interruptOnSpeech: Bool?
let silenceTimeoutMs: Int
@@ -20,6 +83,7 @@ enum TalkModeGatewayConfigParser {
config: [String: Any],
defaultProvider: String,
defaultModelIdFallback: String,
defaultRealtimeModelIdFallback: String,
defaultSilenceTimeoutMs: Int) -> TalkModeGatewayConfigState
{
let talk = TalkConfigParsing.bridgeFoundationDictionary(config["talk"] as? [String: Any])
@@ -29,8 +93,6 @@ enum TalkModeGatewayConfigParser {
allowLegacyFallback: false)
let activeProvider = selection?.provider ?? defaultProvider
let activeConfig = selection?.config
let defaultVoiceId = activeConfig?["voiceId"]?.stringValue?
.trimmingCharacters(in: .whitespacesAndNewlines)
let voiceAliases: [String: String]
if let aliases = activeConfig?["voiceAliases"]?.dictionaryValue {
var resolved: [String: String] = [:]
@@ -45,10 +107,22 @@ enum TalkModeGatewayConfigParser {
} else {
voiceAliases = [:]
}
let model = activeConfig?["modelId"]?.stringValue?.trimmingCharacters(in: .whitespacesAndNewlines)
let model = Self.firstString(activeConfig, keys: ["modelId", "model"])
let defaultModelId = (model?.isEmpty == false) ? model! : defaultModelIdFallback
let defaultOutputFormat = activeConfig?["outputFormat"]?.stringValue?
.trimmingCharacters(in: .whitespacesAndNewlines)
let defaultVoiceId = Self.firstString(activeConfig, keys: ["voiceId", "voice"])
let defaultOutputFormat = Self.firstString(activeConfig, keys: ["outputFormat"])
let realtime = talk?["realtime"]?.dictionaryValue
let realtimeProvider = Self.firstString(realtime, keys: ["provider"])
let realtimeProviders = realtime?["providers"]?.dictionaryValue
let realtimeProviderConfig = Self.realtimeProviderConfig(
providers: realtimeProviders,
provider: realtimeProvider)
let realtimeModel = Self.firstString(realtime, keys: ["model"])
?? Self.firstString(realtimeProviderConfig, keys: ["model"])
let realtimeModelId = realtimeModel ?? defaultRealtimeModelIdFallback
let realtimeVoiceId = Self.firstString(realtime, keys: ["voice"])
?? Self.firstString(realtimeProviderConfig, keys: ["voice"])
let executionMode = Self.resolvedExecutionMode(realtime)
let rawConfigApiKey = activeConfig?["apiKey"]?.stringValue?.trimmingCharacters(in: .whitespacesAndNewlines)
let interruptOnSpeech = talk?["interruptOnSpeech"]?.boolValue
let silenceTimeoutMs = TalkConfigParsing.resolvedSilenceTimeoutMs(
@@ -60,13 +134,53 @@ enum TalkModeGatewayConfigParser {
activeProvider: activeProvider,
normalizedPayload: selection?.normalizedPayload == true,
missingResolvedPayload: talk != nil && selection == nil,
executionMode: executionMode,
defaultVoiceId: defaultVoiceId,
voiceAliases: voiceAliases,
defaultModelId: defaultModelId,
defaultOutputFormat: defaultOutputFormat,
realtimeProvider: realtimeProvider,
realtimeModelId: realtimeModelId,
realtimeVoiceId: realtimeVoiceId,
rawConfigApiKey: rawConfigApiKey,
interruptOnSpeech: interruptOnSpeech,
silenceTimeoutMs: silenceTimeoutMs,
speechLocaleID: speechLocaleID)
}
private static func firstString(_ config: [String: AnyCodable]?, keys: [String]) -> String? {
guard let config else { return nil }
for key in keys {
let value = config[key]?.stringValue?.trimmingCharacters(in: .whitespacesAndNewlines)
if value?.isEmpty == false {
return value
}
}
return nil
}
private static func resolvedExecutionMode(_ realtime: [String: AnyCodable]?) -> TalkModeExecutionMode {
guard let realtime else { return .native }
let mode = Self.firstString(realtime, keys: ["mode"])?.lowercased()
let transport = Self.firstString(realtime, keys: ["transport"])?.lowercased()
let brain = Self.firstString(realtime, keys: ["brain"])?.lowercased()
if mode == "realtime", transport == "gateway-relay", brain == nil || brain == "agent-consult" {
return .realtimeRelay
}
return .native
}
private static func realtimeProviderConfig(
providers: [String: AnyCodable]?,
provider: String?) -> [String: AnyCodable]?
{
guard let providers else { return nil }
if let provider {
return providers[provider]?.dictionaryValue
}
if providers.count == 1 {
return providers.values.first?.dictionaryValue
}
return nil
}
}

View File

@@ -33,6 +33,7 @@ private final class StreamFailureBox: @unchecked Sendable {
final class TalkModeManager: NSObject {
private typealias SpeechRequest = SFSpeechAudioBufferRecognitionRequest
private static let defaultModelIdFallback = "eleven_v3"
private static let defaultRealtimeModelIdFallback = "gpt-realtime-2"
private static let defaultTalkProvider = "elevenlabs"
private static let defaultSilenceTimeoutMs = TalkDefaults.silenceTimeoutMs
private static let redactedConfigSentinel = "__OPENCLAW_REDACTED__"
@@ -47,6 +48,12 @@ final class TalkModeManager: NSObject {
var gatewayTalkApiKeyConfigured: Bool = false
var gatewayTalkDefaultModelId: String?
var gatewayTalkDefaultVoiceId: String?
var gatewayTalkProviderLabel: String = "Not loaded"
var gatewayTalkTransportLabel: String = "Not loaded"
var gatewayTalkUsesRealtimeRelay: Bool = false
var gatewayTalkRealtimeProviderLabel: String?
var gatewayTalkRealtimeModelId: String?
var gatewayTalkRealtimeVoiceId: String?
private enum CaptureMode {
case idle
@@ -84,6 +91,11 @@ final class TalkModeManager: NSObject {
private var voiceOverrideActive = false
private var modelOverrideActive = false
private var defaultOutputFormat: String?
private var activeTalkProvider: String = TalkModeManager.defaultTalkProvider
private var executionMode: TalkModeExecutionMode = .native
private var realtimeProvider: String?
private var realtimeModelId: String?
private var realtimeVoiceId: String?
private var apiKey: String?
private var voiceAliases: [String: String] = [:]
private var interruptOnSpeech: Bool = true
@@ -96,6 +108,7 @@ final class TalkModeManager: NSObject {
private var pcmFormatUnavailable: Bool = false
var pcmPlayer: PCMStreamingAudioPlaying = PCMStreamingAudioPlayer.shared
var mp3Player: StreamingAudioPlaying = StreamingAudioPlayer.shared
private var realtimeRelaySession: RealtimeTalkRelaySession?
private var gateway: GatewayNodeSession?
private var gatewayConnected = false
@@ -164,6 +177,17 @@ final class TalkModeManager: NSObject {
}
}
func applyProviderSelectionChanged() {
let shouldRestart = self.isEnabled
if shouldRestart {
self.stop()
self.isEnabled = true
Task { await self.start() }
} else {
Task { await self.reloadConfig() }
}
}
func start() async {
guard self.isEnabled else { return }
guard self.captureMode != .pushToTalk else { return }
@@ -181,6 +205,11 @@ final class TalkModeManager: NSObject {
self.statusText = "Microphone permission denied"
return
}
await self.reloadConfig()
if self.shouldUseRealtimeRelay() {
await self.startRealtimeRelay()
return
}
let speechOk = await Self.requestSpeechPermission()
guard speechOk else {
self.logger.warning("start blocked: speech permission denied")
@@ -190,7 +219,6 @@ final class TalkModeManager: NSObject {
return
}
await self.reloadConfig()
do {
try Self.configureAudioSession()
// Set this before starting recognition so any early speech errors are classified correctly.
@@ -208,6 +236,58 @@ final class TalkModeManager: NSObject {
}
}
private func shouldUseRealtimeRelay() -> Bool {
self.executionMode == .realtimeRelay
}
private func startRealtimeRelay() async {
guard let gateway else {
self.statusText = "Gateway not connected"
return
}
do {
try Self.configureAudioSession()
self.stopRealtimeRelay()
self.captureMode = .continuous
self.isListening = true
self.statusText = "Connecting realtime…"
let relay = RealtimeTalkRelaySession(
gateway: gateway,
options: RealtimeTalkRelaySession.Options(
sessionKey: self.mainSessionKey,
provider: self.realtimeProvider,
model: self.realtimeModelId,
voice: self.realtimeVoiceId),
pcmPlayer: self.pcmPlayer,
onStatus: { [weak self] status in
self?.statusText = status
},
onSpeakingChanged: { [weak self] speaking in
self?.isSpeaking = speaking
})
self.realtimeRelaySession = relay
try await relay.start()
await self.subscribeChatIfNeeded(sessionKey: self.mainSessionKey)
self.logger.info("realtime relay listening")
let provider = self.realtimeProvider ?? "configured"
let model = self.realtimeModelId ?? "default"
GatewayDiagnostics.log(
"talk realtime: provider=\(provider) model=\(model)")
} catch {
self.realtimeRelaySession = nil
self.isListening = false
self.captureMode = .idle
self.statusText = "Realtime failed: \(error.localizedDescription)"
self.logger.error("realtime relay failed: \(error.localizedDescription, privacy: .public)")
GatewayDiagnostics.log("talk realtime: failed error=\(error.localizedDescription)")
}
}
private func stopRealtimeRelay() {
self.realtimeRelaySession?.stop()
self.realtimeRelaySession = nil
}
func stop() {
self.isEnabled = false
self.isListening = false
@@ -218,6 +298,7 @@ final class TalkModeManager: NSObject {
self.lastHeard = nil
self.silenceTask?.cancel()
self.silenceTask = nil
self.stopRealtimeRelay()
self.stopRecognition()
self.stopSpeaking()
self.lastInterruptedAtSeconds = nil
@@ -263,6 +344,7 @@ final class TalkModeManager: NSObject {
self.silenceTask?.cancel()
self.silenceTask = nil
self.stopRealtimeRelay()
self.stopRecognition()
self.stopSpeaking()
self.lastInterruptedAtSeconds = nil
@@ -1167,6 +1249,9 @@ final class TalkModeManager: NSObject {
}
private func stopSpeaking(storeInterruption: Bool = true) {
if let realtimeRelaySession {
realtimeRelaySession.cancelOutput()
}
let hasIncremental = self.incrementalSpeechActive ||
self.incrementalSpeechTask != nil ||
!self.incrementalSpeechQueue.isEmpty
@@ -1993,26 +2078,87 @@ extension TalkModeManager {
return trimmed
}
private static func displayName(forProvider provider: String) -> String {
switch provider.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() {
case "elevenlabs":
"ElevenLabs"
case "openai":
"OpenAI"
case "google":
"Google"
case let provider where !provider.isEmpty:
provider
default:
"Gateway Default"
}
}
func reloadConfig() async {
guard let gateway else { return }
self.pcmFormatUnavailable = false
do {
let res = try await gateway.request(
method: "talk.config",
paramsJSON: "{\"includeSecrets\":true}",
timeoutSeconds: 8)
guard let json = try JSONSerialization.jsonObject(with: res) as? [String: Any] else { return }
guard let config = json["config"] as? [String: Any] else { return }
func fetchConfig(includeSecrets: Bool) async throws -> [String: Any]? {
let paramsJSON = includeSecrets ? "{\"includeSecrets\":true}" : "{}"
let res = try await gateway.request(
method: "talk.config",
paramsJSON: paramsJSON,
timeoutSeconds: 8)
guard let json = try JSONSerialization.jsonObject(with: res) as? [String: Any] else {
return nil
}
return json["config"] as? [String: Any]
}
let config: [String: Any]
do {
guard let fetched = try await fetchConfig(includeSecrets: true) else { return }
config = fetched
} catch {
let message = String(describing: error)
guard message.contains("operator.talk.secrets"),
let fetched = try await fetchConfig(includeSecrets: false)
else {
throw error
}
config = fetched
GatewayDiagnostics.log("talk config secrets unavailable; loaded redacted config")
}
let parsed = TalkModeGatewayConfigParser.parse(
config: config,
defaultProvider: Self.defaultTalkProvider,
defaultModelIdFallback: Self.defaultModelIdFallback,
defaultRealtimeModelIdFallback: Self.defaultRealtimeModelIdFallback,
defaultSilenceTimeoutMs: Self.defaultSilenceTimeoutMs)
if parsed.missingResolvedPayload {
GatewayDiagnostics.log(
"talk config ignored: normalized payload missing talk.resolved")
}
let activeProvider = parsed.activeProvider
let providerSelection = TalkModeProviderSelection.resolved(
UserDefaults.standard.string(forKey: TalkModeProviderSelection.storageKey))
var activeProvider = parsed.activeProvider
var executionMode = parsed.executionMode
var realtimeProvider = parsed.realtimeProvider
var realtimeModelId = parsed.realtimeModelId
let realtimeVoiceOverride = TalkModeRealtimeVoiceSelection.resolvedOverride(
UserDefaults.standard.string(forKey: TalkModeRealtimeVoiceSelection.storageKey))
let realtimeVoiceId = realtimeVoiceOverride ?? parsed.realtimeVoiceId
switch providerSelection {
case .gatewayDefault:
break
case .nativeElevenLabs:
activeProvider = Self.defaultTalkProvider
executionMode = .native
case .openAIRealtime:
activeProvider = "openai"
executionMode = .realtimeRelay
realtimeProvider = realtimeProvider ?? "openai"
realtimeModelId = realtimeModelId ?? Self.defaultRealtimeModelIdFallback
}
self.activeTalkProvider = activeProvider
self.executionMode = executionMode
self.realtimeProvider = realtimeProvider
self.realtimeModelId = realtimeModelId
self.realtimeVoiceId = realtimeVoiceId
self.defaultVoiceId = parsed.defaultVoiceId
self.voiceAliases = parsed.voiceAliases
if !self.voiceOverrideActive {
@@ -2033,14 +2179,23 @@ extension TalkModeManager {
} else {
self.apiKey = (localApiKey?.isEmpty == false) ? localApiKey : configApiKey
}
if activeProvider != Self.defaultTalkProvider {
if activeProvider != Self.defaultTalkProvider, executionMode != .realtimeRelay {
self.apiKey = nil
GatewayDiagnostics.log(
"talk provider '\(activeProvider)' not yet supported on iOS; using system voice fallback")
}
self.gatewayTalkDefaultVoiceId = self.defaultVoiceId
self.gatewayTalkDefaultModelId = self.defaultModelId
self.gatewayTalkApiKeyConfigured = (self.apiKey?.isEmpty == false)
self.gatewayTalkDefaultVoiceId = executionMode == .realtimeRelay ? realtimeVoiceId : self.defaultVoiceId
self.gatewayTalkDefaultModelId = executionMode == .realtimeRelay ? realtimeModelId : self.defaultModelId
self.gatewayTalkProviderLabel = providerSelection == .gatewayDefault
? Self.displayName(forProvider: activeProvider)
: providerSelection.label
self.gatewayTalkUsesRealtimeRelay = executionMode == .realtimeRelay
self.gatewayTalkTransportLabel = executionMode == .realtimeRelay ? "Gateway relay" : "Native"
self.gatewayTalkRealtimeProviderLabel = realtimeProvider.map { Self.displayName(forProvider: $0) }
self.gatewayTalkRealtimeModelId = realtimeModelId
self.gatewayTalkRealtimeVoiceId = realtimeVoiceId
self.gatewayTalkApiKeyConfigured = executionMode == .realtimeRelay ||
(self.apiKey?.isEmpty == false)
self.gatewayTalkConfigLoaded = true
if let interrupt = parsed.interruptOnSpeech {
self.interruptOnSpeech = interrupt
@@ -2052,6 +2207,17 @@ extension TalkModeManager {
"talk config provider=\(activeProvider) silenceTimeoutMs=\(parsed.silenceTimeoutMs)")
}
} catch {
self.activeTalkProvider = Self.defaultTalkProvider
self.executionMode = .native
self.realtimeProvider = nil
self.realtimeModelId = nil
self.realtimeVoiceId = nil
self.gatewayTalkProviderLabel = "Not loaded"
self.gatewayTalkTransportLabel = "Not loaded"
self.gatewayTalkUsesRealtimeRelay = false
self.gatewayTalkRealtimeProviderLabel = nil
self.gatewayTalkRealtimeModelId = nil
self.gatewayTalkRealtimeVoiceId = nil
self.defaultModelId = Self.defaultModelIdFallback
if !self.modelOverrideActive {
self.currentModelId = self.defaultModelId

View File

@@ -76,6 +76,7 @@ Sources/Voice/TalkDefaults.swift
Sources/Voice/TalkModeGatewayConfig.swift
Sources/Voice/TalkModeManager.swift
Sources/Voice/TalkOrbOverlay.swift
Sources/Voice/RealtimeTalkRelaySession.swift
Sources/Voice/TalkSpeechLocale.swift
Sources/Voice/VoiceTab.swift
Sources/Voice/VoiceWakeManager.swift

View File

@@ -4,6 +4,102 @@ import Testing
@MainActor
@Suite struct TalkModeManagerTests {
@Test func parsesOpenAIRealtimeProviderModelAndVoice() {
let config: [String: Any] = [
"talk": [
"provider": "elevenlabs",
"providers": [
"elevenlabs": [
"modelId": "eleven_v3",
"voiceId": "eleven-voice",
],
],
"resolved": [
"provider": "elevenlabs",
"config": [
"modelId": "eleven_v3",
"voiceId": "eleven-voice",
],
],
"realtime": [
"provider": " openai ",
"model": " gpt-realtime-2 ",
"voice": " marin ",
"mode": "realtime",
"transport": "gateway-relay",
"brain": "agent-consult",
],
],
]
let parsed = TalkModeGatewayConfigParser.parse(
config: config,
defaultProvider: "elevenlabs",
defaultModelIdFallback: "eleven_v3",
defaultRealtimeModelIdFallback: "gpt-realtime-2",
defaultSilenceTimeoutMs: 900)
#expect(parsed.activeProvider == "elevenlabs")
#expect(parsed.executionMode == .realtimeRelay)
#expect(parsed.defaultModelId == "eleven_v3")
#expect(parsed.defaultVoiceId == "eleven-voice")
#expect(parsed.realtimeProvider == "openai")
#expect(parsed.realtimeModelId == "gpt-realtime-2")
#expect(parsed.realtimeVoiceId == "marin")
}
@Test func defaultsOpenAIRealtimeModelWhenProviderOmitsModel() {
let config: [String: Any] = [
"talk": [
"realtime": [
"provider": "openai",
"mode": "realtime",
"transport": "gateway-relay",
],
],
]
let parsed = TalkModeGatewayConfigParser.parse(
config: config,
defaultProvider: "elevenlabs",
defaultModelIdFallback: "eleven_v3",
defaultRealtimeModelIdFallback: "gpt-realtime-2",
defaultSilenceTimeoutMs: 900)
#expect(parsed.executionMode == .realtimeRelay)
#expect(parsed.defaultModelId == "eleven_v3")
#expect(parsed.realtimeModelId == "gpt-realtime-2")
#expect(parsed.realtimeVoiceId == nil)
}
@Test func resolvesRealtimeVoicePickerOverrides() {
#expect(TalkModeRealtimeVoiceSelection.resolvedOverride(nil) == nil)
#expect(TalkModeRealtimeVoiceSelection.resolvedOverride("") == nil)
#expect(TalkModeRealtimeVoiceSelection.resolvedOverride(" Cedar ") == "cedar")
#expect(TalkModeRealtimeVoiceSelection.resolvedOverride("unknown") == nil)
}
@Test func leavesNativeModeWhenRealtimeTransportIsNotGatewayRelay() {
let config: [String: Any] = [
"talk": [
"realtime": [
"provider": "openai",
"mode": "realtime",
"transport": "webrtc",
],
],
]
let parsed = TalkModeGatewayConfigParser.parse(
config: config,
defaultProvider: "elevenlabs",
defaultModelIdFallback: "eleven_v3",
defaultRealtimeModelIdFallback: "gpt-realtime-2",
defaultSilenceTimeoutMs: 900)
#expect(parsed.executionMode == .native)
}
@Test func detectsPCMFormatRejectionFromElevenLabsError() {
let error = NSError(
domain: "ElevenLabsTTS",

View File

@@ -20,9 +20,9 @@
<string>$(OPENCLAW_MARKETING_VERSION)</string>
<key>CFBundleVersion</key>
<string>$(OPENCLAW_BUILD_VERSION)</string>
<key>WKApplication</key>
<true/>
<key>WKCompanionAppBundleIdentifier</key>
<string>$(OPENCLAW_APP_BUNDLE_ID)</string>
<key>WKWatchKitApp</key>
<true/>
</dict>
</plist>

View File

@@ -1,3 +1,3 @@
{
"version": "2026.5.21"
"version": "2026.5.25"
}

View File

@@ -10,6 +10,7 @@ import SwiftUI
@main
struct OpenClawApp: App {
@NSApplicationDelegateAdaptor(AppDelegate.self) private var delegate
@Environment(\.openWindow) private var openWindow
@State private var state: AppState
private static let logger = Logger(subsystem: "ai.openclaw", category: "app")
private let gatewayManager = GatewayProcessManager.shared
@@ -50,6 +51,7 @@ struct OpenClawApp: App {
gatewayStatus: self.gatewayManager.status,
animationsEnabled: self.state.iconAnimationsEnabled && !self.isGatewaySleeping,
iconState: self.effectiveIconState)
.background(SettingsWindowOpenRegistrar())
}
.menuBarExtraAccess(isPresented: self.$isMenuPresented) { item in
self.statusItem = item
@@ -78,13 +80,22 @@ struct OpenClawApp: App {
CLIInstallPrompter.shared.checkAndPromptIfNeeded(reason: "connection-mode")
}
Settings {
Window("OpenClaw Settings", id: SettingsWindowOpener.windowID) {
SettingsRootView(state: self.state, updater: self.delegate.updaterController)
.frame(width: SettingsTab.windowWidth, height: SettingsTab.windowHeight, alignment: .topLeading)
.environment(self.tailscaleService)
}
.defaultSize(width: SettingsTab.windowWidth, height: SettingsTab.windowHeight)
.windowResizability(.contentSize)
.commands {
CommandGroup(replacing: .appSettings) {
Button("Settings...") {
self.openWindow(id: SettingsWindowOpener.windowID)
}
.keyboardShortcut(",", modifiers: .command)
}
SidebarCommands()
}
.onChange(of: self.isMenuPresented) { _, _ in
self.updateStatusHighlight()
self.updateHoverHUDSuppression()
@@ -232,6 +243,21 @@ private final class StatusItemMouseHandlerView: NSView {
}
}
private struct SettingsWindowOpenRegistrar: View {
@Environment(\.openWindow) private var openWindow
var body: some View {
Color.clear
.frame(width: 0, height: 0)
.onAppear {
let openWindow = self.openWindow
SettingsWindowOpener.shared.register {
openWindow(id: SettingsWindowOpener.windowID)
}
}
}
}
@MainActor
final class AppDelegate: NSObject, NSApplicationDelegate {
private var state: AppState?

View File

@@ -18,7 +18,6 @@ struct MenuContent: View {
private let nodesStore = NodesStore.shared
@Bindable private var pairingPrompter = NodePairingApprovalPrompter.shared
@Bindable private var devicePairingPrompter = DevicePairingApprovalPrompter.shared
@Environment(\.openSettings) private var openSettings
@State private var availableMics: [AudioInputDevice] = []
@State private var loadingMics = false
@State private var micObserver = AudioInputDeviceObserver()
@@ -173,9 +172,6 @@ struct MenuContent: View {
self.micRefreshTask = nil
self.micObserver.stop()
}
.task { @MainActor in
SettingsWindowOpener.shared.register(openSettings: self.openSettings)
}
}
private var connectionLabel: String {

View File

@@ -66,7 +66,6 @@ final class OnboardingController {
}
struct OnboardingView: View {
@Environment(\.openSettings) var openSettings
@State var currentPage = 0
@State var isRequesting = false
@State var installingCLI = false

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