Compare commits

..

736 Commits

Author SHA1 Message Date
Patrick Erichsen
c4c364cd27 fix: mark approval gateway calls as runtime clients 2026-05-17 21:24:42 -07:00
Peter Steinberger
5980c0d807 fix: wrap Mac menu gateway errors 2026-05-18 05:21:19 +01:00
Ayaan Zaidi
1c778f7afb fix(telegram): repair desktop proof login 2026-05-18 09:49:21 +05:30
Peter Steinberger
84b34519a8 fix: preflight remote skill bin probes 2026-05-18 05:19:02 +01:00
Peter Steinberger
71ed6526b1 ci: reduce aggregate runner jobs 2026-05-18 04:53:40 +01:00
Peter Steinberger
8483d03375 fix(gateway): preserve spawned sessions in configured lists 2026-05-18 04:38:14 +01:00
Peter Steinberger
696b4863c3 chore: quiet autoreview default fallback 2026-05-18 04:37:19 +01:00
Vincent Koc
a642ca9a89 ci(qa-lab): schedule live token efficiency artifacts 2026-05-18 11:33:13 +08:00
Vincent Koc
1300b22630 fix(qa-lab): classify runtime token efficiency 2026-05-18 11:09:08 +08:00
Peter Steinberger
29653e4106 fix: harden Mac gateway transport selection 2026-05-18 04:06:17 +01:00
Peter Steinberger
1ba3368fa6 fix: clean up Mac settings sidebar controls 2026-05-18 04:06:17 +01:00
Vincent Koc
4dec9679e6 fix(qa-lab): gate missing runtime tool coverage 2026-05-18 11:00:20 +08:00
Ayaan Zaidi
1ab84b4327 docs(changelog): note telegram 421 retry (#48908) (thanks @MarsDoge) 2026-05-18 08:28:27 +05:30
Dongyan Qian
63b728de43 fix(telegram): retry 421 misdirected request responses
Treat Telegram HTTP 421 / Misdirected Request responses as retryable transport failures in both the default channel API retry policy and the strict outbound send retry path.

Wire the 421 handling into isSafeToRetrySendError so non-idempotent Telegram send operations can retry this edge-node rejection without enabling broad ambiguous network retries, and add regression coverage for the default retry path plus strict send predicate handling.
2026-05-18 08:28:27 +05:30
Vincent Koc
73ca3cf3c3 test: tolerate optional ACP cron live timeout 2026-05-18 10:55:13 +08:00
Peter Steinberger
11d7499db1 feat: extend autoreview fallback reviewers 2026-05-18 03:49:23 +01:00
Galin Iliev
ad55d486ce fix(github-copilot): sanitize unsafe reasoning replay ids (#83221)
Fixes #83220.
2026-05-17 19:48:27 -07:00
Gio Della-Libera
1b5bc33161 fix(doctor): archive legacy clawd browser profile residue (#83230)
* fix(doctor): archive legacy clawd browser profile residue

* Avoid browser cleanup load without residue

Doctor --fix now skips loading the browser doctor facade unless the legacy browser/clawd profile path exists, preventing broad config repair tests from paying the plugin load cost when there is nothing to archive.

* Use structured health check for browser residue

Register the legacy clawd browser profile residue cleanup through the modern doctor health-check contract so doctor --lint can report it and doctor --fix repairs it through structured effects.
2026-05-17 19:45:03 -07:00
Gio Della-Libera
bcbe8b6299 fix(codex): surface declined native tool replies (#83108) 2026-05-17 19:43:19 -07:00
Galin Iliev
bc4f27c89a ci: skip changelog-only workflow runs (#83215)
Summary
Problem: root CHANGELOG.md updates currently cause broad pull request and push workflow activity, including CI and workflow sanity fanout, even though changelog-only edits do not touch product, runtime, docs site, or workflow logic.
Why it matters: the PR workflow (review, prepare, and land) can add or adjust CHANGELOG.md entries while processing otherwise-ready PRs. Those changelog-only updates retrigger gates, delay landing, and create avoidable contention when several PRs are being landed close together.
What changed: CI now ignores pull requests whose only changed path is CHANGELOG.md; Workflow Sanity ignores changelog-only pull requests and main-branch pushes; Docs keeps its markdown/docs trigger but excludes root CHANGELOG.md from the push path set.
What did NOT change (scope boundary): metadata-only automation such as labelers, auto-response, real behavior proof, or external GitHub apps can still run on PR events because those workflows are event-driven rather than file-scope CI. Other markdown files, docs files, and workflow files still trigger their existing checks.
2026-05-17 19:29:45 -07:00
Ayaan Zaidi
6baa2b38b2 ci(mantis): make telegram proof skips public-safe 2026-05-18 07:54:11 +05:30
Peter Steinberger
48f7db23f0 fix: harden clawpatch-reported edge cases 2026-05-18 03:18:55 +01:00
Tak Hoffman
816fbe0cf0 chore(labels): cool label palette (#83374)
* chore(labels): cool label palette

* chore(labels): soften taxonomy colors

* chore(labels): finalize label palette

* chore(labels): harden final palette
2026-05-17 21:12:10 -05:00
Peter Steinberger
69cea57f69 fix(telegram): fail closed on missing topic threads (#83381)
* fix(telegram): fail closed on missing topic threads

* docs(changelog): reference telegram topic cleanup
2026-05-18 03:07:12 +01:00
Vincent Koc
58e1351863 fix(qa-lab): hard gate runtime tool coverage 2026-05-18 10:05:04 +08:00
Peter Steinberger
73f4657869 docs: require autoreview before PR landing 2026-05-18 03:02:48 +01:00
Gio Della-Libera
1768667374 fix(migrate): count hidden config conflicts in preview (#83314) 2026-05-17 18:50:22 -07:00
Gio Della-Libera
8855a4aa58 fix(update): require integer timeout values (#83310)
* fix(update): require integer timeout values

* fix(update): reject blank timeout values
2026-05-17 18:47:59 -07:00
Peter Steinberger
4b4048fd22 fix: guard xai oauth callback cors (#83322) (thanks @Jaaneek) 2026-05-18 02:43:12 +01:00
Jaaneek
5f1df99a9c xai: OAuth login fixes plus openclaw User-Agent attribution
OAuth login flow
----------------
- Hard-require refresh_token after the authorization-code exchange in
  xai-oauth.ts. Access-only responses persisted credentials that the
  downstream usability check later rejected; the new requireRefreshToken
  option fails the exchange instead. Error wording explains the missing
  refresh_token in OIDC scope terms (offline_access scope rejected),
  not a "grant".
- Derive token expiry from the access-token JWT exp claim when
  expires_in is missing. id_token exp is intentionally not used as a
  fallback because id_token lifetime tracks the OIDC session, not the
  access token, and would defer refresh past actual expiry.
- Handle CORS preflight OPTIONS on the loopback OAuth callback in
  src/plugin-sdk/provider-auth-runtime.ts. The previous handler treated
  any non-callback request as a failed GET, returned "Missing code or
  state", and tore the server down before the real GET arrived. The
  CORS allowlist is now an optional `corsOriginAllowlist` parameter on
  waitForLocalOAuthCallback so the SDK helper stays generic. The xAI
  plugin passes ["auth.x.ai", "accounts.x.ai"] from loginXaiOAuth.

Sidecar surfaces
----------------
- speech-provider.ts (POST /v1/tts) honors the xAI OAuth profile in
  addition to provider config and XAI_API_KEY. isConfigured now also
  reports true when an xAI auth profile is configured (via
  isProviderAuthProfileConfigured), so OAuth-only users are no longer
  silently filtered out by the selection layer. The bearer resolver
  threads req.cfg into resolveApiKeyForProvider so the right xAI auth
  profile is picked when a user has multiple.
- realtime-transcription-provider.ts (WSS /stt) gets the same
  isConfigured fix, and the lazy headers() resolver threads req.cfg
  into the OAuth bearer lookup. createSession stays sync per its
  plugin contract.
- stt.ts: drop the plugin-side OAuth fallback. The media-understanding
  core already resolves auth (cfg/agentDir-aware) via
  resolveProviderExecutionContext before calling transcribeAudio, so
  the wrapper was redundant. transcribeAudio is now the registered
  hook directly.

User-Agent attribution
----------------------
- New buildXaiAttributionPolicy in src/agents/provider-attribution.ts
  injects User-Agent: openclaw/<version>, originator, and version on
  /v1/responses and /v1/chat/completions traffic that goes through
  resolveProviderRequestHeaders. Gated to xai-native and default
  endpoint classes; custom proxy baseUrls remain withheld. reviewNote
  is honest about which headers are spec-verified vs mirrored.
- Shared extensions/xai/src/xai-user-agent.ts helper exports
  xaiUserAgentHeaderFor(baseUrl) which only emits the User-Agent when
  the resolved baseUrl points at the xAI-native API host. Threaded
  through TTS and realtime STT (WS upgrade headers) so user-configured
  proxy baseUrls do not receive the openclaw identity. OAuth discovery
  and token endpoints still send User-Agent unconditionally because
  isTrustedXaiOAuthEndpoint already restricts those URLs to *.x.ai.
- Image gen, batch STT, and video gen rely on the attribution policy
  alone (no manual User-Agent in defaultHeaders), so attribution
  withholding on user-configured proxy baseUrls is preserved
  end-to-end.
- UA is bearer-agnostic: same value whether the bearer comes from an
  xAI API key or the xAI OAuth flow.

Drop dead api.grok.x.ai alias
-----------------------------
- xAI retired the api.grok.x.ai alias; DNS now returns NXDOMAIN from
  xAI's own authoritative nameservers. Drop it from the xai-native
  endpoint host set in extensions/xai/openclaw.plugin.json,
  extensions/xai/api.ts, extensions/xai/tts.ts, and the
  openai-responses payload policy. Update the attribution test to
  classify api.grok.x.ai as "custom" (no live user can reach it; the
  classification keeps documenting the host's status).

Video generation now matches xAI's actual API behavior
------------------------------------------------------
Previously, real video generation requests failed with
"xAI video generation response malformed" because the poll-status
handler validated against a closed enum that did not match what the
xAI service actually returns. Four fixes:
- Loosen the poll-status handler. xAI returns intermediate strings
  outside `["queued", "processing", "done", "failed", "expired"]`
  (commonly `submitted`, `pending`, `in_progress`, ...). Treat `done`
  as terminal-success, `["failed", "error", "expired", "cancelled"]`
  as terminal-failure, and any other string (including empty) as
  continue-polling. Also accept `cancelled` as a terminal failure.
- Send default duration/aspect_ratio/resolution on every generate and
  reference-image submit. xAI rejects bodies that omit these fields.
  Defaults: duration=8s, aspect_ratio="16:9", resolution="720p".
- Accept lowercase resolution input ("480p"/"720p"/"1080p") in
  addition to uppercase, normalize to lowercase on the wire.
- Add an `x-idempotency-key` header (fresh `crypto.randomUUID()`) on
  every submit so a network retry does not double-charge the user.
  Polls intentionally reuse the unmodified `headers` without the key.

Ergonomics
----------
- All "missing xAI credentials" errors (code_execution, lazy
  code_execution fallback in extensions/xai/index.ts, x_search,
  web_search grok in web-search-provider.runtime.ts, TTS, batch STT,
  realtime STT) now mention `openclaw onboard --auth-choice xai-oauth`
  first.
- Dedupe the Grok model-id alias table: model-compat.ts re-exports
  normalizeXaiModelId from model-id.ts as normalizeNativeXaiModelId.

Test coverage
-------------
- src/plugin-sdk/provider-auth-runtime.test.ts: locks the new pure
  buildOAuthCallbackOriginResolver gate (allowlist match,
  case-normalization, https-only, non-allowlisted hosts dropped,
  multi-Origin handling).
- extensions/xai/xai-oauth.test.ts: locks
  XAI_OAUTH_CALLBACK_CORS_ORIGIN_ALLOWLIST so loginXaiOAuth keeps
  threading the right hosts to the SDK helper.
- extensions/xai/speech-provider.test.ts: OAuth-only auth profile
  flips isConfigured to true; cfg threads into the OAuth fallback
  resolver.
- extensions/xai/realtime-transcription-provider.test.ts: same +
  upgrade headers carry the OAuth bearer end-to-end.
- extensions/xai/stt.test.ts: explicit assertion that transcribeAudio
  trusts the core-resolved apiKey (no plugin-side wrapper).

Verification
------------
- pnpm install: clean
- 154/154 vitest tests pass across 13 touched test files
- pnpm check:changed: typecheck core/ext + tests, oxlint core/ext,
  runtime guards, dependency pin guard, package patch guard, runtime
  import cycles, sidecar loader guard - all green
- pnpm build: 0 errors, 0 [INEFFECTIVE_DYNAMIC_IMPORT] warnings
2026-05-18 02:43:12 +01:00
Peter Steinberger
b5046968f6 docs: clarify media completion handoff 2026-05-18 02:36:17 +01:00
Peter Steinberger
645ef817b6 test(channels): preserve thread origin contracts
Add core and hook mapper regression coverage for the thread-origin contract behind #83302.\n\nThe tests prove a flat reply target can coexist with a thread-addressable OriginatingTo, and hook canonical conversation mapping keeps following OriginatingTo.\n\nProof: focused Vitest, autoreview, Testbox check:changed tbx_01krwaztbwm13sx9e4sbyyz4c1, and CI run 26008670388 passed.
2026-05-18 02:30:24 +01:00
Peter Steinberger
9aa46843ec fix(telegram): preserve forum topic origin targets
Fix Telegram forum-topic OriginatingTo routing for inbound, audio-preflight, and skipped-message hook contexts.

Centralize Telegram inbound origin target construction so real forum topics stay encoded in the routing target while DM thread ids remain metadata-only.

Fixes #83302.
2026-05-18 02:19:46 +01:00
Josh Avant
73049d291b Fix transcript-only assistant rows in latest reply lookup (#83362)
* fix: skip transcript-only latest assistant rows

* chore: add changelog for transcript-only assistant fix
2026-05-17 20:13:34 -05:00
Tak Hoffman
7ff8323ed5 chore(labels): add label color sync policy (#83357)
* chore(labels): add label color sync script

* chore(labels): align future label colors
2026-05-17 20:09:47 -05:00
Peter Steinberger
5434769e47 fix(cron): suppress source replies for announce delivery 2026-05-18 01:41:16 +01:00
Peter Steinberger
428fc16ac8 ci: make Tideclaw alpha long gates advisory 2026-05-18 01:40:37 +01:00
compoodment
6ebe91d92b test: cover one-chunk progress final payload 2026-05-18 01:37:59 +01:00
Peter Steinberger
2d2c420ed2 test: speed up prompt snapshot checks 2026-05-18 01:37:31 +01:00
Peter Steinberger
3d85e84df3 test(ci): update prerelease runner expectation 2026-05-18 01:35:04 +01:00
Peter Steinberger
bb691a0d25 fix(ci): recognize gateway run command chunk 2026-05-18 01:35:04 +01:00
Peter Steinberger
9bdc183b7d fix(cli): keep subcommand help lightweight 2026-05-18 01:35:04 +01:00
Peter Steinberger
b0b18d1e4a fix: seed control UI origins for bind aliases 2026-05-18 01:21:33 +01:00
Peter Steinberger
17ab3b11cb ci: reduce main workflow queue time 2026-05-18 01:18:50 +01:00
Peter Steinberger
91266fa928 fix(telegram): bound isolated long-poll timeout 2026-05-18 01:05:27 +01:00
Peter Steinberger
47a2efe483 fix: hide display-hidden chat transcript messages 2026-05-18 01:04:48 +01:00
Peter Steinberger
9da0f80356 fix(openai): allow available Codex OAuth models 2026-05-18 01:04:14 +01:00
Peter Steinberger
77bbffb998 docs: run autoreview with full-access sandbox 2026-05-18 00:58:30 +01:00
Peter Steinberger
bef3356375 fix(macos): keep dashboard failures in window 2026-05-18 00:56:28 +01:00
Peter Steinberger
086d3d012e docs: add maintainer assignment triage workflow 2026-05-18 00:52:37 +01:00
VACInc
72e164a3fe fix: preserve recent Codex context projections 2026-05-18 00:41:36 +01:00
Josh Avant
06f4c97130 Keep legacy Codex OAuth sidecar profiles usable (#83312)
* fix legacy Codex oauth sidecar compatibility

* docs add changelog for legacy Codex oauth compatibility

* annotate legacy oauth hash compatibility
2026-05-17 18:41:07 -05:00
Peter Steinberger
9a936b3063 test: fix CI regressions 2026-05-18 00:37:48 +01:00
Peter Steinberger
691d62630f test: keep slow tests under duration cap 2026-05-18 00:26:44 +01:00
Peter Steinberger
7bcd5acc1a test(codex): type denied tool policy mocks (#82374) (thanks @VACInc) 2026-05-18 00:18:20 +01:00
VACInc
5f1d8a2ee4 fix(codex): fail closed restricted native tools 2026-05-18 00:18:20 +01:00
VACInc
dad3db40d3 fix(codex): honor denied app-server tool policy 2026-05-18 00:18:20 +01:00
wAngByg
d63c581dec fix(gemini-transport): validate thought_signature base64 before forwarding to Gemini (#82995)
Merged via squash.

Prepared head SHA: 8634757622
Co-authored-by: wAngByg <281221101+wAngByg@users.noreply.github.com>
Co-authored-by: altaywtf <9790196+altaywtf@users.noreply.github.com>
Reviewed-by: @altaywtf
2026-05-18 02:16:13 +03:00
Peter Steinberger
7afac6015f feat(browser): surface observed dialogs (#83099) 2026-05-18 00:05:29 +01:00
JC
57da466ecb Fix Discord verbose tool progress delivery (#80042)
Summary:
- The PR changes Discord reply delivery, sanitizer, and queued follow-up auto-reply paths so explicit verbose tool-progress payloads are delivered while final assistant replies still use the privacy sanitizer.
- Reproducibility: yes. source-level: current main strips tool-looking Discord payload text at the front-chann ... ds compaction events in queued follow-up runs. I did not run a live Discord repro in this read-only review.

Automerge notes:
- Ran the ClawSweeper repair loop before final review.
- Included post-review commit in the final squash: fix: gate queued follow-up progress when verbose is off
- Included post-review commit in the final squash: fix: preserve queued verbose progress under preview suppression
- Included post-review commit in the final squash: ci: rerun discord verbose progress PR
- Included post-review commit in the final squash: fix: preserve Discord verbose progress after rebase
- Included post-review commit in the final squash: fix: serialize discord queued progress
- Included post-review commit in the final squash: Fix Discord verbose tool progress delivery

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

Prepared head SHA: fd845e773a
Review: https://github.com/openclaw/openclaw/pull/80042#issuecomment-4414121881

Co-authored-by: Clawsistant <clawsistant@users.noreply.github.com>
Co-authored-by: anyech <anyech@gmail.com>
Co-authored-by: OpenClaw Assistant <assistant@openclaw.local>
Co-authored-by: Shadow <hi@shadowing.dev>
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: thewilloftheshadow
Co-authored-by: thewilloftheshadow <35580099+thewilloftheshadow@users.noreply.github.com>
2026-05-17 22:59:07 +00:00
Peter Steinberger
127f3f86d7 style(macos): align sessions settings padding 2026-05-17 23:56:52 +01:00
mjamiv
c93d6d8daa fix(gateway): keep unmanaged restarts in-process (#83138)
Summary:
- The PR changes ordinary unmanaged gateway restarts to return the existing in-process fallback instead of detached-spawning a replacement child, with focused tests, docs wording, and a changelog entry.
- Reproducibility: yes. at source level: current main and v2026.5.12 detach-spawn unmanaged ordinary restarts, ... e PR body also supplies after-fix terminal proof that the patched helper returns disabled without spawning.

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

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

Prepared head SHA: 8c82df6c77
Review: https://github.com/openclaw/openclaw/pull/83138#issuecomment-4471071848

Co-authored-by: mjamiv <74088820+mjamiv@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-17 21:19:05 +00:00
VACInc
aa71f7fe15 Fix Telegram stop debounce bypass (#83248)
Summary:
- The PR adds a generic inbound debounce `cancelKey`, uses Telegram stop-like controls to cancel same-chat pen ... buffers and bypass debounce, and adds focused Telegram regression coverage plus updated channel test mocks.
- Reproducibility: yes. by source inspection: current main enqueues Telegram text through inbound debounce bef ... nly has flush semantics for pending keyed work. I did not run a live Telegram repro in this read-only pass.

Automerge notes:
- PR branch already contained follow-up commit before automerge: Fix Telegram stop debounce bypass

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

Prepared head SHA: 19245a341d
Review: https://github.com/openclaw/openclaw/pull/83248#issuecomment-4472300906

Co-authored-by: VACInc <3279061+VACInc@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-17 21:10:53 +00:00
Kevin Lin
d85a7c6b67 docs: fix building plugin typebox import 2026-05-17 13:36:09 -07:00
Kevin Lin
d0736919aa docs: clean up building plugins guide
Refactor docs/plugins/building-plugins.md into the scoped plugin-author guide, preserving the legacy registering-agent-tools anchor and restoring the original Next steps section.
2026-05-17 13:32:15 -07:00
clawsweeper[bot]
bacc18a575 Log Telegram outbound delivery success (#83247)
Summary:
- The PR adds info-level Telegram outbound send success logs for text/media sends, tracks accepted threadless  ... s, and loads the OpenAI Codex external auth overlay for Codex plugin-harness runs with regression coverage.
- Reproducibility: yes. there is a source-level reproduction path: the branch adds focused tests for Telegram  ... mission/privacy and Codex auth overlay selection. I did not execute those tests in this read-only checkout.

Automerge notes:
- PR branch already contained follow-up commit before automerge: Use Codex auth overlay for scoped Codex runs
- PR branch already contained follow-up commit before automerge: Add regression tests for Codex auth and Telegram send logs
- PR branch already contained follow-up commit before automerge: Log Telegram outbound delivery success

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

Prepared head SHA: b860487aef
Review: https://github.com/openclaw/openclaw/pull/83247#issuecomment-4472287527

Co-authored-by: jrwrest <jrwrest@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-17 19:46:31 +00:00
Gio Della-Libera
9a5f2f61e7 Doctor: add health-check contract and --lint validation (#80055)
* feat(doctor): add --lint mode + structured HealthFinding shape

Adds the core machinery for `openclaw doctor --lint` per the
doctor-lint-and-oc-rules upstream proposal. PR-1 of the proposal:
no new top-level verb, no public plugin SDK; everything internal.

Files:
- src/flows/checks.ts ? HealthFinding / HealthCheck / HealthCheckContext
   types. Findings carry severity per-finding; checks return
   readonly HealthFinding[]. Mode tag (doctor/lint/fix) lets a check
   distinguish the calling posture.
- src/flows/health-check-registry.ts ? module-level registry with
   duplicate-id rejection + test reset helper.
- src/flows/doctor-lint-flow.ts ? runner over registered checks.
   Catches throws into synthetic error findings (anchored at check id;
   message scrubbed of control chars, capped at 256 bytes). Sorts
   findings by severity desc, check id, path. Exports
   exitCodeFromFindings (1 if any warning/error, 0 otherwise).
- src/flows/doctor-core-checks.ts ? 4 modern HealthChecks rewriting
   logic from existing legacy run*Health functions:
     core/doctor/gateway-config            (warning)
     core/doctor/command-owner             (info)
     core/doctor/workspace-status          (info)
     core/doctor/final-config-validation   (error)
   Each was audited safe per the proposal's adapter constraints
   (no writes, no repair calls, no prompts, no probes incl. local-bind).
   Legacy run*Health contributions in doctor-health-contributions.ts
   are unchanged ? doctor mode (no --lint) still runs the existing 35.
- src/commands/doctor-lint.ts ? CLI dispatch for --lint. Reads config
   snapshot, builds HealthCheckContext (mode: "lint"), runs the registry,
   filters by --severity-min, emits human or JSON output, returns exit
   code from unfiltered set so --severity-min hides info findings
   without changing CI signal.
- src/cli/program/register.maintenance.ts ? adds --lint, --json,
   --severity-min, --skip, --only flags to existing doctor command.
   --lint branches to runDoctorLintCli; without --lint, doctor runs
   unchanged.

LoC: 382 src across 6 files. Tests + doc + oc-path-side rule packs
follow as separate commits on this branch.

* fix: avoid string spread in doctor errors

* chore: refresh plugin SDK API baseline

* docs: clarify doctor lint usage

* feat(doctor): prepare repairs for dry-run reporting
2026-05-17 12:29:57 -07:00
Tak Hoffman
0dc04fb926 ci(mantis): allow ClawSweeper telegram proof agent (#83243) 2026-05-17 14:26:15 -05:00
Gio Della-Libera
fb53c2d610 fix(doctor): detect stale session snapshot paths (#82867)
* fix(doctor): detect stale session snapshot paths

Warn when cached session snapshot metadata still references bundled skill paths from inactive OpenClaw runtime roots, while keeping workspace skill roots and current runtime paths quiet.

* fix(doctor): honor configured session stores

* fix(doctor): scan raw snapshot paths

Expand home-relative cached snapshot paths before stale bundled-skill classification and scan raw session-store JSON so persisted resolvedSkills are inspected before normal session-store normalization strips them.
2026-05-17 12:12:25 -07:00
clawsweeper[bot]
214f718be7 fix(agents): persist subagent registry before returning accepted (#83132) (#83238)
Summary:
- This PR adds a strict initial subagent registry persistence path, rolls back failed registrations, updates affected test seams, adds a regression test, and records the fix in the changelog.
- Reproducibility: yes. Source inspection on current main shows registry save failures are swallowed after the ... s added, and the linked source PR provides an ENOSPC-style after-fix terminal proof for the corrected path.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(agents): persist subagent registry before returning accepted (#83…

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

Prepared head SHA: d564ef051d
Review: https://github.com/openclaw/openclaw/pull/83238#issuecomment-4472173642

Co-authored-by: yetval <yetvald@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-17 19:11:01 +00:00
clawsweeper[bot]
f36a1b0c81 fix(codex): preserve streamed command output (#83222)
Summary:
- The PR buffers Codex command-output deltas per command item and uses them as a fallback for transcripts, trajectory output, final tool output, and after-tool-call errors when `aggregatedOutput` is empty.
- Reproducibility: yes. A source-level reproduction is clear: send current-turn command-output delta notificat ... aggregatedOutput: null`; current main has no final transcript or trajectory fallback for the streamed text.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(codex): preserve streamed command output

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

Prepared head SHA: 07393a304f
Review: https://github.com/openclaw/openclaw/pull/83222#issuecomment-4472054629

Co-authored-by: 0x505badc0de <32790662+rozmiarD@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-17 18:41:00 +00:00
clawsweeper[bot]
3e765263dd fix(agents): preserve run-mode keep subagents past session sweep TTL (#83132) (#83226)
Summary:
- The PR exempts run-mode `cleanup: "keep"` subagent registry entries from the session-mode sweep TTL, adds focused regression coverage, and records the fix in the changelog.
- Reproducibility: yes. Current main source shows a run-mode keep entry has no `archiveAtMs` and then matches  ... ; the linked source PR also provides before/after terminal proof against a real persisted `runs.json` path.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(agents): preserve run-mode keep subagents past session sweep TTL …

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

Prepared head SHA: 32faf5cf32
Review: https://github.com/openclaw/openclaw/pull/83226#issuecomment-4472073823

Co-authored-by: yetval <yetvald@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-17 18:27:53 +00:00
clawsweeper[bot]
fb028cadc8 fix(codex): deliver Telegram verbose tool progress (#83214)
Summary:
- The branch updates Codex app-server tool-progress projection and auto-reply dispatch so Telegram direct mess ... l-only `/verbose` turns deliver concise tool summaries while filtering message-send and activity-log noise.
- Reproducibility: yes. Current-main source inspection shows `message_tool_only` suppression can drop verbose tool summaries before dispatch, and the linked source PR gives a live Telegram DM before/after path.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(codex): deliver Telegram verbose tool progress

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

Prepared head SHA: f6a79cb306
Review: https://github.com/openclaw/openclaw/pull/83214#issuecomment-4471954529

Co-authored-by: Tyler Bea <43728897+kurplunkin@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-17 18:23:58 +00:00
Peter Steinberger
800a0d3166 test: stabilize live subagent steering 2026-05-17 18:45:44 +01:00
sandypockets
a5a5df67da Fix clipped usage chart tooltip (#82846)
Summary:
- The PR replaces per-bar absolute Usage chart tooltips with one viewport-fixed floating tooltip and adds focus/keyboard handling plus focused jsdom coverage.
- Reproducibility: yes. at source level. Current main renders an absolute `.daily-bar-tooltip` inside `.daily- ... ` overflow contexts, and the linked issue plus PR before screenshot demonstrate the tall-bar clipping case.

Automerge notes:
- PR branch already contained follow-up commit before automerge: Merge branch 'main' into fix-usage-tooltip-clipping

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

Prepared head SHA: edbb26a5be
Review: https://github.com/openclaw/openclaw/pull/82846#issuecomment-4468967811

Co-authored-by: sandypockets <41454557+sandypockets@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
2026-05-17 17:25:23 +00:00
Tak Hoffman
0f1f9525f3 fix(ci): clear Mantis command reactions (#83194)
* fix(ci): clear mantis command reactions

* fix(ci): clear Mantis command reactions

---------

Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
2026-05-17 12:22:01 -05:00
Peter Steinberger
c3308b9195 test: keep matrix subagent spawn opt-in 2026-05-17 18:20:54 +01:00
100menotu001
7c416950c6 fix(feishu): return subagent thread delivery origin (#83190)
Summary:
- The PR returns a Feishu/Lark deliveryOrigin from subagent_spawning after successful thread-bound session binding, adds DM/topic/sender-scoped topic hook assertions, and adds a changelog entry.
- Reproducibility: yes. by source inspection. Current main's Feishu subagent_spawning hook binds the child con ... eneric session-spawn path only directly routes the initial child run when result.deliveryOrigin is present.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(feishu): return subagent thread delivery origin

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

Prepared head SHA: 44a6200a91
Review: https://github.com/openclaw/openclaw/pull/83190#issuecomment-4471452247

Co-authored-by: OpenClaw Contributor <100menotu001@users.noreply.github.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
2026-05-17 17:12:06 +00:00
Peter Steinberger
59b85d4eb9 test: stabilize release validation flakes 2026-05-17 18:04:35 +01:00
Gio Della-Libera
44c3d8ea2e fix(memory): preserve qmd lexical search for hyphenated queries (#81423) 2026-05-17 09:52:04 -07:00
clawsweeper[bot]
893f580072 fix(update): tailor gateway recovery hints by platform (#83191)
Summary:
- The PR updates the CLI post-update gateway recovery formatter and tests to show Linux, macOS, Windows, or generic service-manager guidance, plus a changelog entry.
- Reproducibility: yes. Source inspection gives a high-confidence reproduction path: current main reaches a fo ... hAgent recovery text, while the platform contract says Linux uses systemd and Windows uses Scheduled Tasks.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(update): tailor gateway recovery hints by platform

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

Prepared head SHA: 0cf2a0c5a7
Review: https://github.com/openclaw/openclaw/pull/83191#issuecomment-4471471293

Co-authored-by: Rubén Cuevas <hi@rubencu.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
2026-05-17 16:48:08 +00:00
Peter Steinberger
af62fd45cd test: stabilize release qa gates 2026-05-17 17:45:58 +01:00
Peter Steinberger
6ebc5e4719 test: harden release qa edge scenarios 2026-05-17 17:26:37 +01:00
Tak Hoffman
f349fb82aa fix(mantis): remove ambiguous github trigger mention (#83179) 2026-05-17 11:24:23 -05:00
Vincent Koc
79212f9869 feat(qa-lab): select runtime parity tiers 2026-05-18 00:21:13 +08:00
Gavin Zeng
ea72414e1c fix(build): bundle zod inline to fix pnpm global install resolution (#78515)
Merged via squash.

Prepared head SHA: c925d1afab
Co-authored-by: ggzeng <20488795+ggzeng@users.noreply.github.com>
Co-authored-by: altaywtf <9790196+altaywtf@users.noreply.github.com>
Reviewed-by: @altaywtf
2026-05-17 19:20:42 +03:00
Chris Zhang
ac848d318d fix(agents): exclude tool result details from guard budget (#75525)
Merged via squash.

Prepared head SHA: 4efe094507
Co-authored-by: zqchris <4436110+zqchris@users.noreply.github.com>
Co-authored-by: altaywtf <9790196+altaywtf@users.noreply.github.com>
Reviewed-by: @altaywtf
2026-05-17 19:14:59 +03:00
Peter Steinberger
4c60ab3666 test: extend release qa wait windows 2026-05-17 17:05:15 +01:00
Vincent Koc
9249e13891 test(qa-lab): sync personal pack expectation 2026-05-17 23:56:18 +08:00
Vincent Koc
94c0d9ac81 docs(changelog): backfill qa-lab runtime coverage notes 2026-05-17 23:56:17 +08:00
Ayaan Zaidi
59efd95669 ci(mantis): add telegram proof label trigger 2026-05-17 21:16:00 +05:30
Vincent Koc
b764396dee fix(qa-lab): differentiate mock provider plans 2026-05-17 23:44:35 +08:00
Firas Alswihry
45a434fb23 test(qa-lab): add personal approval denial scenario 2026-05-17 23:33:09 +08:00
clawsweeper[bot]
1760881574 fix(plugins): default 15s timeout for before_agent_start hook (#48534) (#83147)
Summary:
- The PR adds a 15-second default timeout for legacy `before_agent_start` modifying hooks, regression tests for hung handlers, and a changelog fix entry.
- Reproducibility: yes. Registering a `before_agent_start` handler that returns a never-settling promise is en ... ts the hook and the runner awaits directly; the linked source PR also supplies before/after terminal proof.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(plugins): default 15s timeout for before_agent_start hook (#48534)

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

Prepared head SHA: 8d2c5b8808
Review: https://github.com/openclaw/openclaw/pull/83147#issuecomment-4471169756

Co-authored-by: Rahul <rahulnilvan43@gmail.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
2026-05-17 15:31:32 +00:00
Peter Steinberger
a00e494992 test: harden matrix subagent spawn prompt 2026-05-17 16:21:31 +01:00
github-actions[bot]
428af92ac9 chore(ui): refresh fa control ui locale 2026-05-17 15:20:13 +00:00
github-actions[bot]
40d8e6eab7 chore(ui): refresh nl control ui locale 2026-05-17 15:20:08 +00:00
github-actions[bot]
1e0e2c0e2d chore(ui): refresh vi control ui locale 2026-05-17 15:19:59 +00:00
github-actions[bot]
303effce67 chore(ui): refresh th control ui locale 2026-05-17 15:19:55 +00:00
github-actions[bot]
96f2e1ae43 chore(ui): refresh pl control ui locale 2026-05-17 15:19:37 +00:00
github-actions[bot]
0ce7cb1b7f chore(ui): refresh id control ui locale 2026-05-17 15:19:24 +00:00
github-actions[bot]
364f8cd04f chore(ui): refresh uk control ui locale 2026-05-17 15:19:19 +00:00
github-actions[bot]
bcef46e63c chore(ui): refresh tr control ui locale 2026-05-17 15:19:13 +00:00
github-actions[bot]
747cfbbaad chore(ui): refresh it control ui locale 2026-05-17 15:19:00 +00:00
github-actions[bot]
f4776138c7 chore(ui): refresh ar control ui locale 2026-05-17 15:18:54 +00:00
github-actions[bot]
e536ce86bf chore(ui): refresh ko control ui locale 2026-05-17 15:18:38 +00:00
github-actions[bot]
ed6c46ec7e chore(ui): refresh es control ui locale 2026-05-17 15:18:33 +00:00
github-actions[bot]
4a6b50c789 chore(ui): refresh fr control ui locale 2026-05-17 15:18:27 +00:00
github-actions[bot]
b76fac10dc chore(ui): refresh ja-JP control ui locale 2026-05-17 15:18:22 +00:00
github-actions[bot]
1e446845fd chore(ui): refresh de control ui locale 2026-05-17 15:17:50 +00:00
github-actions[bot]
ce634337d2 chore(ui): refresh zh-TW control ui locale 2026-05-17 15:17:48 +00:00
github-actions[bot]
c2adcd0a36 chore(ui): refresh zh-CN control ui locale 2026-05-17 15:17:46 +00:00
github-actions[bot]
135005e3dd chore(ui): refresh pt-BR control ui locale 2026-05-17 15:17:42 +00:00
Vincent Koc
1926982c4c fix(qa-lab): refresh parity model targets 2026-05-17 23:12:26 +08:00
吴杨帆
019dbcc749 fix(failover): classify Moonshot balance 429 as billing (#83079)
Merged via squash.

Prepared head SHA: 9f70bf5935
Co-authored-by: leno23 <39647285+leno23@users.noreply.github.com>
Co-authored-by: altaywtf <9790196+altaywtf@users.noreply.github.com>
Reviewed-by: @altaywtf
2026-05-17 18:06:07 +03:00
Peter Steinberger
72eef85942 ci: raise qa live build heap 2026-05-17 16:05:16 +01:00
Gio Della-Libera
164c35da85 fix(cli): lower extra gateway advisory severity (#82922)
Merged via squash.

Prepared head SHA: abed98a5f3
Co-authored-by: giodl73-repo <235387111+giodl73-repo@users.noreply.github.com>
Co-authored-by: altaywtf <9790196+altaywtf@users.noreply.github.com>
Reviewed-by: @altaywtf
2026-05-17 18:03:03 +03:00
Peter Steinberger
543518bd43 test: accept runtime matrix dm approvals 2026-05-17 15:58:07 +01:00
Peter Steinberger
833f1ce735 test: finish fanout responses followup 2026-05-17 15:42:46 +01:00
Vincent Koc
66b8de9c83 fix(qa-lab): preflight live codex auth 2026-05-17 22:39:00 +08:00
Peter Steinberger
aaf85166de test: complete mock fanout followup 2026-05-17 15:31:59 +01:00
Peter Steinberger
7554deef30 test: retry transient Slack live timeouts 2026-05-17 15:11:41 +01:00
Peter Steinberger
f0fc8c27d3 test: settle Slack gateway handoff 2026-05-17 15:02:40 +01:00
Peter Steinberger
ef763d0f0b test: ignore unrelated Slack no-reply observations 2026-05-17 14:55:22 +01:00
Peter Steinberger
395346fe57 test: wait for stable Slack live readiness 2026-05-17 14:47:48 +01:00
Peter Steinberger
2ab76240d3 test: align qa docker UI dist assertion 2026-05-17 14:39:27 +01:00
Ayaan Zaidi
bb64223155 docs(changelog): add Android TLS prompt PR reference (#83077) (thanks @sliekens) 2026-05-17 19:02:29 +05:30
Ayaan Zaidi
30263f6d35 refactor(android): distill TLS fingerprint prompt flow 2026-05-17 19:02:29 +05:30
Steven Liekens
848e0486b7 fix(android): prompt on changed TLS thumbprint 2026-05-17 19:02:29 +05:30
Peter Steinberger
cdd817669a fix: preserve Slack presentation fallback 2026-05-17 14:18:23 +01:00
Peter Steinberger
9d85f05b01 fix: keep chunkable presentation text intact 2026-05-17 14:18:23 +01:00
Peter Steinberger
5dbc969b46 fix: preserve Telegram interactive precedence 2026-05-17 14:18:23 +01:00
Peter Steinberger
2a6ef5287b fix: align agent presentation button type 2026-05-17 14:18:23 +01:00
Peter Steinberger
a4210dbaee docs: refresh plugin sdk api baseline 2026-05-17 14:18:23 +01:00
Peter Steinberger
b78c2ee8c8 refactor: adopt presentation rendering in Mattermost 2026-05-17 14:18:23 +01:00
Peter Steinberger
f5090d2624 feat: render Teams presentations as Adaptive Cards 2026-05-17 14:18:23 +01:00
Peter Steinberger
fee1cd9867 docs: document presentation API surface 2026-05-17 14:18:23 +01:00
Peter Steinberger
ee72ce8cf7 refactor: deprecate legacy interactive reply APIs 2026-05-17 14:18:23 +01:00
Peter Steinberger
ad861d4c9d feat: add presentation capability limits 2026-05-17 14:18:23 +01:00
Peter Steinberger
868315aef0 test: fix install readdir mock typing 2026-05-17 14:12:55 +01:00
Peter Steinberger
798833140d test: fix WhatsApp live readiness typing 2026-05-17 14:08:06 +01:00
Vincent Koc
d3a7348ad9 fix(qa-lab): keep bootstrap tokens private 2026-05-17 21:05:01 +08:00
Peter Steinberger
5fc6b714a6 test: extend live transport stability windows 2026-05-17 13:54:57 +01:00
Peter Steinberger
e43a2efcdb test: harden wsl2 fixtures 2026-05-17 13:45:21 +01:00
Vincent Koc
c80cb5986f fix(update): use post-doctor plugin records 2026-05-17 20:41:59 +08:00
Peter Steinberger
22723b6f1e test: harden live transport gates 2026-05-17 13:41:04 +01:00
Vincent Koc
9bb5f5af0d fix(qa-lab): fail live parity without token usage 2026-05-17 20:38:10 +08:00
Peter Steinberger
3b39ff4318 test: tolerate degraded live transport state 2026-05-17 13:25:23 +01:00
Peter Steinberger
60fc982cb6 fix(macos): avoid cron settings crash 2026-05-17 13:22:32 +01:00
Vincent Koc
9b62a35760 fix(qa-lab): expose redacted qa bus tool traces 2026-05-17 20:18:48 +08:00
Peter Steinberger
f74b302dc2 test: harden live QA transport validation 2026-05-17 13:16:02 +01:00
Peter Steinberger
18ac38963d chore: remove old codex review skill path 2026-05-17 13:15:48 +01:00
Peter Steinberger
006ebe692d chore: rename codex review skill to autoreview 2026-05-17 13:15:30 +01:00
Peter Steinberger
4f4be666eb chore: update codex review fallback handling 2026-05-17 13:11:35 +01:00
Vincent Koc
e3621f5057 fix(cli): keep secret diagnostics off json stdout 2026-05-17 20:08:16 +08:00
Vincent Koc
f9c8cb7877 fix(feishu): refresh inbound session routes 2026-05-17 20:05:10 +08:00
Peter Steinberger
3c6ec521d5 fix(cli): resolve PowerShell completion profiles 2026-05-17 12:58:35 +01:00
Stellar鱼
fe68af5307 fix(cli): show concrete PowerShell completion profile path 2026-05-17 12:58:35 +01:00
Peter Steinberger
93db190308 fix(gateway): isolate hot reload channel failures
* fix(gateway): isolate hot reload channel failures

* fix(gateway): restore partial hot reload stops
2026-05-17 12:56:14 +01:00
Peter Steinberger
9897559e3f test: accept final-only Matrix quiet streaming replies 2026-05-17 12:56:01 +01:00
Alex Knight
8a060b2904 Release embedded session write lock before model I/O (#82891)
Summary:
- The PR narrows embedded PI session transcript write-lock scope, adds stale/max-hold config plumbing, and updates affected transcript, doctor, gateway, SDK, Codex mirroring, docs, and regression-test surfaces.
- Reproducibility: yes. Current main source still holds the embedded session write lock from early attempt set ... cksmith Testbox contention proof on unmodified main; I did not rerun the live repro in this read-only pass.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(agents): narrow context engine session lock
- PR branch already contained follow-up commit before automerge: fix session lock runner build types
- PR branch already contained follow-up commit before automerge: Release embedded session write lock before model I/O
- PR branch already contained follow-up commit before automerge: fix(clawsweeper): address review for automerge-openclaw-openclaw-8289…

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

Prepared head SHA: 4c6dd7ed6e
Review: https://github.com/openclaw/openclaw/pull/82891#issuecomment-4469282923

Co-authored-by: Alex Knight <15041791+amknight@users.noreply.github.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
2026-05-17 11:54:03 +00:00
Peter Steinberger
3dd8bcb419 style(macos): polish settings panes 2026-05-17 12:41:27 +01:00
Peter Steinberger
5e1fde7c22 ci: serialize WhatsApp live QA jobs 2026-05-17 12:40:31 +01:00
Peter Steinberger
0aae5ba077 test: retry WhatsApp QA driver observation timeouts 2026-05-17 12:32:46 +01:00
Peter Steinberger
8f59a370aa test: harden live QA retry handling 2026-05-17 12:23:19 +01:00
Peter Steinberger
4aa671b71a fix: use declared crawl module paths (#83040) 2026-05-17 12:18:23 +01:00
Peter Steinberger
4ccd07718d chore: point crawl skills at openclaw repos 2026-05-17 12:18:23 +01:00
Peter Steinberger
09f7702b96 feat: add crawl archive skills 2026-05-17 12:18:23 +01:00
Jerry-Xin
3e9e1d6321 fix: route subagent announce to originating parent session instead of channel-bound peer session (#80242)
* fix: route subagent announce to originating parent session instead of channel-bound peer session

When a subagent is spawned from agent:main:main while a Telegram DM is active,
the completion announce was delivered to the parallel Telegram channel session
instead of the originating parent.

Two interacting bugs:

1. The spawn tool received the sandbox/policy session key (Telegram peer key)
   as the requester, instead of the real run session key. Fixed by passing
   runSessionKey to createSessionsSpawnTool so the registered requester
   points to the actual parent session.

2. resolveSubagentCompletionOrigin checked child session bindings before
   requester bindings. When both share the same channel+accountId (common
   for Telegram DMs), the child binding hijacked the delivery target.
   Fixed by checking requester binding first, with child as fallback.

Fixes #80201

* fix: drop subagent_announce from mediated completion set

The subagent_announce addition to AGENT_MEDIATED_COMPLETION_TOOLS was
unrelated to the routing fix and could cause group/channel completions
to fail silently when the subagent does not use the message tool.

This should be addressed separately with proper message-tool-only
guidance (tracked in #80223).

* fix: separate sandbox policy from completion owner in sessions_spawn

PR #80242 passed runSessionKey as agentSessionKey to createSessionsSpawnTool,
which caused spawnSubagentDirect to use the run session key for sandbox policy
checks (resolveSandboxRuntimeStatus). This could make a sandboxed channel run
appear unsandboxed.

Introduce completionOwnerKey as a separate field that is only used for
registerSubagentRun routing (requesterSessionKey), keeping agentSessionKey
for sandbox enforcement, callerDepth, activeChildren, and all other policy
checks.

* fix(agents): preserve subagent ownership routing

---------

Co-authored-by: 忻役 <xinyi@mininglamp.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-05-17 12:17:51 +01:00
Vincent Koc
d1cd74b243 fix(channels): scope dm last-route updates 2026-05-17 19:11:28 +08:00
Vincent Koc
7d6e45ef7c fix(qa-lab): clean orphaned gateway runtimes 2026-05-17 19:10:46 +08:00
Peter Steinberger
045d7aae50 docs: update obsidian skill for official cli 2026-05-17 12:09:34 +01:00
Peter Steinberger
7bf4dfeff3 test: harden live QA transport probes 2026-05-17 12:08:45 +01:00
Rui Xu
d41916b5c3 fix(memory): clarify vector degradation warning 2026-05-17 12:08:37 +01:00
Vincent Koc
9a50fe1497 changelog: note setTimeout yield for Responses stream abort timers 2026-05-17 19:02:51 +08:00
Kaspre
69a0c925b8 fix(codex): cover side-question native hooks (#82559)
* fix(codex): cover side-question native hooks

* fix(codex): enforce native approvals for app-server requests

* fix(codex): preserve approval fallback after native relay noop

* fix(codex): satisfy approval relay json typing

* fix(codex): run approval relay in report mode

* fix(codex): keep relay pre-tool decisions deny-only

* fix(codex): remove dead relay approval branch

* fix(codex): dedupe app-server relay approvals

* fix(codex): fail closed on native relay rewrites

* fix(codex): preserve side-question provider context

* fix(codex): route side-question replies to origin

* fix(codex): preserve native hook channel context

* test(codex): align native relay rewrite assertion

* fix(codex): align side-question hook config

* fix(codex): route side-question approvals safely

* test(codex): fix side-question hook typing

* fix(codex): preserve side-question hook policy context

* fix(codex): close native hook relay review gaps

* fix(codex): keep dynamic tool hook channel context

* fix(codex): preserve native finalize hook channel context

* fix(codex): scope dynamic tool result hooks by channel

* fix(codex): drop stale deadcode allowlist entry

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-05-17 12:02:17 +01:00
Peter Steinberger
3fcc8b19ba feat(skills): add python debugpy skill 2026-05-17 11:56:31 +01:00
Peter Steinberger
ee492092a7 fix: yield responses streams to abort timers 2026-05-17 11:53:48 +01:00
Ayaan Zaidi
9e0386563f docs(changelog): note telegram media group warning (#82987) (thanks @eldar702) 2026-05-17 16:23:06 +05:30
Ayaan Zaidi
be934c0347 fix(telegram): warn on all failed media groups 2026-05-17 16:23:06 +05:30
eldar702
066ca3926a fix(telegram): enable the media-group skip-warning guard [AI-assisted]
The warning branch added in the previous commit was committed with an
always-false guard (`if (false && skippedCount > 0 && ...)`), so the
notification never fired — flagged by review as [P1]. Remove the
`false &&` so partial-album media loss actually notifies the user, as
the accompanying tests already expect.

Refs #55216
2026-05-17 16:23:06 +05:30
eldar702
9a45c0701b fix(telegram): warn when a media group silently drops failed photos [AI-assisted]
Telegram albums where some photos failed to download were processed
silently: the agent received only the photos that resolved, and the
user was never told images had been lost.

processMediaGroup now tracks a skippedCount (incremented on a
recoverable per-photo fetch error and on a null resolveMedia result).
When at least one photo still resolved, it emits a single anchored
warning per album (never per photo) using the same
withTelegramApiErrorLogging wrapper + swallowed-send pattern as the
existing single-attachment "Failed to download media" notice. The
all-failed-album case is intentionally left silent (out of scope).

Fixes #55216
2026-05-17 16:23:06 +05:30
Peter Steinberger
decbd611a0 docs: refresh embedded skill guidance 2026-05-17 11:50:27 +01:00
Peter Steinberger
d8198c8c0e fix: use Codex runtime context budget for compaction 2026-05-17 11:46:17 +01:00
Peter Steinberger
084318b8c4 docs: add Codex app-server guard changelog 2026-05-17 11:45:59 +01:00
Peter Steinberger
403fbd7296 fix: address Codex guard review findings 2026-05-17 11:45:59 +01:00
Peter Steinberger
a6908fac16 fix: honor custom Codex home for rollout guards 2026-05-17 11:45:59 +01:00
Peter Steinberger
4008ba56fc test: fix Codex app-server budget guard types 2026-05-17 11:45:59 +01:00
Peter Steinberger
e8e4b93a94 fix: harden Codex rollout budget scanning 2026-05-17 11:45:59 +01:00
Peter Steinberger
8e9961a945 fix: tighten Codex app-server budget guards 2026-05-17 11:45:59 +01:00
Han Kim
f86a0c8c9a Guard Codex app-server context budgets 2026-05-17 11:45:59 +01:00
Peter Steinberger
156e86afa4 fix: load source tool plugin entries with SDK aliases 2026-05-17 11:45:18 +01:00
Peter Steinberger
3dbe37c694 docs: refresh llm-task generated manifest 2026-05-17 11:45:18 +01:00
Peter Steinberger
439612bf56 docs: refresh plugin SDK API baseline 2026-05-17 11:45:18 +01:00
Peter Steinberger
4d05008283 fix: preserve tool plugin manifest metadata 2026-05-17 11:45:18 +01:00
Peter Steinberger
ae172741e1 feat: dogfood tool plugin helpers 2026-05-17 11:45:18 +01:00
Peter Steinberger
b95c8a4d95 docs: add tool plugin authoring guide 2026-05-17 11:45:18 +01:00
Peter Steinberger
b17e4ed50c feat: add simple tool plugin authoring 2026-05-17 11:45:18 +01:00
Peter Steinberger
0e76dafe42 test: avoid telegram startup abort deadlock 2026-05-17 11:42:37 +01:00
Peter Steinberger
51e93669cb test: relax oc-path perf budget in ci 2026-05-17 11:37:11 +01:00
Vincent Koc
10dd9c5aee fix(e2e): follow scoped configure prompts 2026-05-17 18:30:07 +08:00
Peter Steinberger
0165560f70 test: align plugin metadata test snapshots 2026-05-17 11:29:39 +01:00
Peter Steinberger
9feca3e11e fix: stabilize release validation gates 2026-05-17 11:24:01 +01:00
Peter Steinberger
8dd91b14d3 fix(google): recover Gemini tool-call thought signatures
Fixes #72879.
Supersedes contributor PR #80358; fork push was blocked despite maintainer edits being enabled.

Co-authored-by: abnershang <abner.shang@gmail.com>
2026-05-17 11:16:47 +01:00
Vincent Koc
5aac7939db fix(gateway): drain replies during restart close 2026-05-17 18:12:52 +08:00
hcl
42435d110b fix(browser): derive Chrome launch readiness from a single CDP diagnostic (#82904) (#82986)
* fix(browser): derive Chrome launch readiness from a single CDP diagnostic (#82904)

The pre-fix launch path used `isChromeReachable` (a lightweight HTTP
`/json/version` probe) to decide failure, then called the stronger
`diagnoseChromeCdp` only to format the thrown error. On macOS cold
starts where the HTTP probe transiently fails *between* the polling
loop and the diagnostic call, the runtime would throw

    "Failed to start Chrome CDP on port ... { ok: true, wsUrl: ... }"

— a self-contradicting error containing a successful diagnostic
result. Per #82904 this is the actual user-visible bug.

Capture `diagnoseChromeCdp` ONCE after the polling loop and use it for
both the decision and the error text. The diagnostic helper already
includes the lightweight reachability check and adds a websocket
`Browser.getVersion` health command, so it is strictly stronger than
the HTTP probe; if `diagnoseChromeCdp` returns ok the launch
genuinely succeeded.

The existing `withMockChromeCdpServer` success test in
chrome.internal.test.ts still exercises this code path end-to-end
(real HTTP server + real websocket handshake), so the regression-safety
case is covered. The asymmetric `probe-fails-but-diagnostic-succeeds`
scenario is hard to mock without restructuring the existing test
harness; this commit ships the fix and relies on the upstream
ClawSweeper review criteria (manual managed-Chrome cold-start proof)
plus the standalone real-behavior probe in the PR body.

* fix(browser): import ChromeCdpDiagnostic type from chrome.diagnostics

The annotation `let finalDiagnostic: ChromeCdpDiagnostic | null` referenced
a type that was only re-exported (not imported) inside chrome.ts, causing
oxlint/tsc to read it as the implicit `error` type and fail check-lint,
check-prod-types, check-test-types, etc. Add the type to the existing
chrome.diagnostics.js import block.

* fix(browser): preserve Chrome launch diagnostic fallback

* test(browser): satisfy launch diagnostic lint

* fix(browser): keep Chrome launch readiness scoped

* test(browser): answer CDP launch mock probe

---------

Co-authored-by: hclsys <hclsys@users.noreply.github.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-05-17 11:11:15 +01:00
Peter Steinberger
bf51933358 docs(skills): use neutral skill wording 2026-05-17 11:04:04 +01:00
Peter Steinberger
7e2d6ef06f fix(skills): keep spike scratch inside workspace 2026-05-17 11:04:04 +01:00
Peter Steinberger
0591b31388 feat(skills): add debugger diagram and spike skills
# Conflicts:
#	CHANGELOG.md
2026-05-17 11:04:04 +01:00
Vincent Koc
b8a6a387ee changelog: note gateway secrets startup fast path 2026-05-17 18:02:28 +08:00
Vincent Koc
540a4a73d5 fix(ci): handle missing SwiftLint in Testbox changed checks 2026-05-17 18:00:19 +08:00
Josh Avant
903d9c13f3 Fix subagent completion announce delivery timing (#83039)
* fix subagent announce transcript delivery

* chore changelog for subagent announce delivery

* test align subagent retry suspension expectation
2026-05-17 04:59:58 -05:00
Peter Steinberger
0177a4b6c9 fix(gateway): speed up secrets startup
Summary:
- Split the lightweight secrets runtime state and auth-store cache from the full secrets runtime.
- Use the startup fast path whenever gateway startup has no SecretRef values, while preserving cleanup and refresh semantics.
- Add regression coverage for startup-only empty auth-store snapshots and update affected gateway/tool tests.

Verification:
- pnpm test src/secrets/runtime.fast-path.test.ts src/secrets/runtime-state.test.ts src/gateway/server-startup-config.secrets.test.ts src/gateway/server-import-boundary.test.ts src/gateway/server-aux-handlers.test.ts src/gateway/server-methods/config.shared-auth.test.ts src/agents/tools/web-tools.enabled-defaults.test.ts src/agents/tools/web-tool-runtime-context.test.ts -- --reporter=verbose
- pnpm build
- pnpm format:check -- src/agents/tools/web-tools.enabled-defaults.test.ts src/secrets/runtime-command-secrets.ts src/secrets/runtime-fast-path.ts src/secrets/runtime.fast-path.test.ts src/agents/auth-profiles/store.ts src/agents/auth-profiles/store-cache.ts src/secrets/runtime-state.ts src/secrets/runtime-state.test.ts src/gateway/server-startup-config.ts
- codex-review --mode branch
- isolated gateway token-auth smoke: openclaw gateway run + openclaw gateway health returned ok: true
- GitHub CI on PR #83031 green; newer Real behavior proof run passed on current SHA f27ed3f7ce.

Co-authored-by: samzong <samzong.lu@gmail.com>
2026-05-17 10:55:41 +01:00
Josh Avant
f29bcff4da fix(models): reuse plugin metadata snapshot (#83033)
* fix(models): reuse plugin metadata snapshot

* docs: add models performance changelog

* test: satisfy models metadata fixture types
2026-05-17 04:51:59 -05:00
Peter Steinberger
9616aa6e5a build(protocol): refresh gateway secrets models 2026-05-17 10:42:57 +01:00
Peter Steinberger
d66fe50a10 fix(cli): preserve optional web fallback secrets
Co-authored-by: wuyangfan <1102042793@qq.com>
2026-05-17 10:42:57 +01:00
Peter Steinberger
e3a248585e fix(cli): preserve scoped secret resolution
Co-authored-by: wuyangfan <1102042793@qq.com>

# Conflicts:
#	src/cli/capability-cli.test.ts
#	src/cli/capability-cli.ts
2026-05-17 10:42:57 +01:00
Peter Steinberger
fe680e47ce fix(cli): scope web command secret refs 2026-05-17 10:42:57 +01:00
wuyangfan
230806eaf2 fix(cli): resolve plugin web search SecretRefs for infer web search
Materialize agent-runtime plugin credentials through the shared command
secret resolution path before local web search/fetch runs, matching gateway
runtime behavior for plugins.entries.*.config.webSearch.apiKey refs.

Fixes openclaw/openclaw#82621

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-17 10:42:57 +01:00
Peter Steinberger
6f024293e0 fix: surface reply media failures
Co-authored-by: Jerry-Xin <jerryxin0@gmail.com>
2026-05-17 10:39:28 +01:00
Vincent Koc
5d4dac690c changelog: note qa-lab multipass temp root 2026-05-17 17:37:36 +08:00
Peter Steinberger
2df393886a test: align cron schema description assertions 2026-05-17 10:34:20 +01:00
Peter Steinberger
6eeba8cfb4 docs: note clean tool schema cleanup 2026-05-17 10:34:20 +01:00
Peter Steinberger
9b698ce0d6 refactor: shorten agent tool descriptions 2026-05-17 10:34:20 +01:00
Vincent Koc
cfb032797f fix(qa-lab): wake stale cursor long polls 2026-05-17 17:33:47 +08:00
Peter Steinberger
3e6902236c style(mac): refine settings panes 2026-05-17 10:31:04 +01:00
Vincent Koc
a4bea46a35 fix(codex): preserve nested tool-result middleware output 2026-05-17 17:30:58 +08:00
Vincent Koc
37dcf385e5 fix(qa): expose codex tools for runtime parity 2026-05-17 17:20:12 +08:00
Vincent Koc
2c9f68f42b changelog: note qa-lab tool coverage command 2026-05-17 17:17:55 +08:00
Vincent Koc
1f9d8c1e9d fix(qa-lab): wire tool coverage report command 2026-05-17 17:12:10 +08:00
Vincent Koc
00fc2950d9 chore(scripts): harden dev tooling diagnostics 2026-05-17 17:04:18 +08:00
Vincent Koc
54d063167e test: use platform spy helper in cli tests 2026-05-17 17:03:23 +08:00
Vincent Koc
673596013e changelog: note Together thinking format config support 2026-05-17 17:02:29 +08:00
Peter Steinberger
7b96109920 ci: include skill scripts in duplicate scan 2026-05-17 09:58:24 +01:00
Peter Steinberger
b7704b917e feat(skills): add meme maker skill 2026-05-17 09:58:24 +01:00
Vincent Koc
85f8fd0533 test: reuse platform spy helper in infra tests 2026-05-17 16:58:18 +08:00
Vincent Koc
9ca98a6d39 fix(config): accept together thinking format 2026-05-17 16:55:51 +08:00
Vincent Koc
d217fd7a92 test(qa-lab): add runtime tool fixtures 2026-05-17 16:55:50 +08:00
Vincent Koc
46061442e7 test: share process platform spy helper 2026-05-17 16:52:46 +08:00
samzong
3c1c850c02 fix(acpx): keep startup probe in runtime service
Signed-off-by: samzong <samzong.lu@gmail.com>
2026-05-17 09:47:20 +01:00
Peter Steinberger
5425ecc1aa style(macos): apply SwiftFormat 2026-05-17 09:46:30 +01:00
Josh Avant
8ba2dfa76a Fix message tool session-key route drift (#83004)
* fix message tool session-key route drift

* docs changelog for message tool session-key route
2026-05-17 03:36:14 -05:00
Peter Steinberger
69d588cf2a fix(openai): remove GPT reply brevity cap 2026-05-17 09:29:11 +01:00
Vincent Koc
37806afd2d chore(plugins): bump tokenjuice to 0.7.1 2026-05-17 16:23:26 +08:00
Josh Avant
022723829a fix(agents): preserve suspended subagent final deliveries (#82999)
* fix: preserve suspended subagent final deliveries

* chore: update changelog for subagent delivery fix

* test: use valid killed subagent outcome fixture
2026-05-17 03:23:15 -05:00
Bob
80d03a1e5b fix: classify provider conversation state errors (#82616)
Classify provider conversation-state rejections and return a clear message-channel error instead of auto-resetting or falling back to a generic runner failure.

Local validation:
- pnpm docs:list
- pnpm build
- pnpm check
- node scripts/run-vitest.mjs src/auto-reply/reply/provider-request-error-classifier.test.ts src/auto-reply/reply/agent-runner-execution.test.ts src/auto-reply/reply/dispatch-from-config.test.ts
- node scripts/run-vitest.mjs run --config test/vitest/vitest.e2e.config.ts src/auto-reply/reply/agent-runner.runreplyagent.e2e.test.ts

Co-authored-by: dutifulbob <261991368+dutifulbob@users.noreply.github.com>
2026-05-17 16:22:09 +08:00
Vincent Koc
2547e35b0a test(qa-lab): add harness sentinel scenarios 2026-05-17 16:19:58 +08:00
Peter Steinberger
3cfac6d430 fix(browser): harden CLI wait and option handling 2026-05-17 09:19:14 +01:00
Vincent Koc
3918d69587 fix(agents): skip malformed transcript state entries (#82624)
* fix(agents): skip malformed transcript state entries

* fix(agents): preserve repairable transcript tool calls

* fix(agents): preserve openclaw transcript content blocks

* fix(agents): preserve string tool call arguments

* fix(agents): keep repaired compaction markers on branch

* fix(agents): keep legacy assistant transcript text

* fix(agents): preserve null tool call arguments

* fix(agents): keep transcript repair chains

* fix(agents): drop labels for rejected transcript rows

* fix(agents): preserve legacy transcript compaction indexes

* fix(agents): drop unresolved transcript repair parents
2026-05-17 16:18:23 +08:00
Peter Steinberger
e53ba8fcf5 fix(provider): use Together video API endpoint
Route Together video generation through the v2 video API even when shared Together text config points at the v1 base URL.

Verification:
- pnpm test extensions/together/video-generation-provider.test.ts
- pnpm check:test-types
- git diff --check
- codex-review --parallel-tests "pnpm test extensions/together/video-generation-provider.test.ts"
- gh pr checks 82992 --watch --fail-fast=false
2026-05-17 09:16:44 +01:00
samzong
dd32c5307f fix(gateway): reuse startup auth preflight snapshot
Summary:
- Reuse the prepared gateway startup auth SecretRef snapshot when the startup config still matches the preflight source.
- Preserve fresh activation fallback for config mismatches and shared weak-token, warning, and recovery handling.
- Add focused regression coverage and changelog entry.

Verification:
- pnpm test src/gateway/server-startup-config.secrets.test.ts
- GitHub checks green on 72587758ee
2026-05-17 01:12:21 -07:00
Peter Steinberger
83e19ca469 fix: keep ACP turns on OpenClaw timeouts (#82997) 2026-05-17 09:10:42 +01:00
Peter Steinberger
e4ec1b3de8 test(cli): clarify gateway model-run session proof (#82861) (thanks @Kaspre) 2026-05-17 09:08:02 +01:00
Kaspre
820ec9d3be test(cli): assert fresh gateway model-run sessions 2026-05-17 09:08:02 +01:00
Kaspre
0fd152b286 fix(cli): isolate gateway model run sessions 2026-05-17 09:08:02 +01:00
Peter Steinberger
a5b1177b68 fix(codex): preserve agent scope for bound app-server sessions 2026-05-17 09:03:05 +01:00
Peter Steinberger
993fe3ef0f fix(mac): polish settings window chrome 2026-05-17 08:59:32 +01:00
Peter Steinberger
a985c99059 fix: steer song requests to music generation 2026-05-17 08:57:27 +01:00
Vincent Koc
327b0b8734 changelog: credit Codex native tool trajectory recording 2026-05-17 15:50:41 +08:00
Peter Steinberger
5d1f7bf058 fix: route image URL describes through MiniMax VLM
Summary:
- Preserve HTTP image describe inputs as remote media.
- Route MiniMax CN image understanding through MiniMax-VL-01.
- Cover CLI, media runtime, tools, Telegram stickers, docs, and changelog.

Verification:
- codex-review clean
- pnpm check:changed via Blacksmith Testbox tbx_01krtdekwak0mygxbw5z7cfb6z
- PR CI green on 516281448e
2026-05-17 08:45:50 +01:00
Peter Steinberger
9a36e897be fix: surface async media generation failures 2026-05-17 08:40:33 +01:00
Peter Steinberger
635b947e32 fix(acp): honor terminal turn results 2026-05-17 08:25:14 +01:00
Peter Steinberger
1f6ababb63 fix(mac): keep settings panes warm 2026-05-17 08:18:27 +01:00
Evgeny Yurchenko
592aae3696 fix: avoid idle Codex hook relay subprocesses
Avoid installing Codex native PostToolUse/Stop hook relays when OpenClaw has no matching local handlers. This keeps pre-tool safety and permission approval relays active while removing idle no-op subprocess fan-out.

Fixes #76552.

Co-authored-by: evgyur <evgyur@users.noreply.github.com>
2026-05-17 08:17:51 +01:00
Peter Steinberger
6b688ed614 docs: clarify ambient room recommendation 2026-05-17 08:16:58 +01:00
Peter Steinberger
77547226ce fix: improve progress draft truncation 2026-05-17 08:13:39 +01:00
Peter Steinberger
76da34760c fix(mac): speed up config settings 2026-05-17 08:03:10 +01:00
joshavant
c30c8cb471 test(tasks): expect classified ACP stall update 2026-05-17 01:50:07 -05:00
Peter Steinberger
d1638f1185 fix(codex): record native tool trajectories
Co-authored-by: vyctorbrzezowski <krzyszchweski@gmail.com>
2026-05-17 07:43:28 +01:00
Josh Avant
7d99f8b021 fix(gateway): allow trusted-proxy local-direct password fallback (#82953)
* fix(gateway): restore trusted-proxy local password fallback

* docs(changelog): note trusted-proxy password fallback fix

* docs(changelog): clarify trusted-proxy fallback policy
2026-05-17 01:35:59 -05:00
Galin Iliev
8dc213227b docs: add prompt cache changelog 2026-05-17 06:30:50 +00:00
Galin Iliev
a656f887c8 fix: satisfy OpenAI tool payload lint 2026-05-17 06:30:50 +00:00
Galin Iliev
afdb8705e9 fix: stabilize OpenAI tool payload ordering 2026-05-17 06:30:49 +00:00
YB0y
6720aa9c42 fix(cli): add sessions list alias (#81163) (thanks @YB0y) 2026-05-17 07:27:53 +01:00
Galin Iliev
aca258a8a9 fix: explain memory compaction tool allowlist warnings
Fixes #82941.
2026-05-16 23:25:00 -07:00
Peter Steinberger
aaadf721e3 fix(agents): classify ACP no-output stalls 2026-05-17 07:23:41 +01:00
Peter Steinberger
a46d2e2b06 docs: add ambient room events guide 2026-05-17 07:20:15 +01:00
Vincent Koc
684a6303b3 changelog: add OpenAI Codex runtime routing and OAuth bootstrap (#82864) 2026-05-17 14:15:56 +08:00
Peter Steinberger
669786595d fix(logging): avoid false liveness backlog warnings 2026-05-17 07:15:17 +01:00
Josh Avant
9a063e38d1 Fix TTS supplement delivery across live previews (#82935)
* fix: avoid duplicated tts supplement replies

* chore: add changelog for tts supplement fix
2026-05-17 01:15:12 -05:00
Zavian Wang
9a11e76458 fix(plugins): surface configured runtime plugin doctor warnings (#81674)
Fixes #81326.

Summary:
- Warn from `openclaw plugins doctor` when configured runtime owner plugins are missing, disabled, or blocked.
- Share configured-runtime plugin install mapping with `openclaw doctor --fix`, including ACP/acpx.
- Keep implicit OpenAI Codex preferences quiet to avoid false-positive plugin doctor warnings.

Verification:
- `pnpm test src/agents/harness-runtimes.test.ts src/cli/plugins-cli.list.test.ts src/commands/doctor/shared/missing-configured-plugin-install.test.ts -- --reporter=verbose`
- `pnpm exec oxfmt --check CHANGELOG.md src/agents/harness-runtimes.ts src/agents/harness-runtimes.test.ts src/cli/plugins-cli.runtime.ts src/cli/plugins-cli.list.test.ts src/commands/doctor/shared/configured-runtime-plugin-installs.ts src/commands/doctor/shared/missing-configured-plugin-install.ts`
- `pnpm build:plugin-sdk:dts`
- `codex-review --mode branch`
- Testbox-through-Crabbox `pnpm check:changed`: provider `blacksmith-testbox`, id `tbx_01krt8vte22m7ht6wfss4jkeaa`, Actions run https://github.com/openclaw/openclaw/actions/runs/25983150787, exit 0

Co-authored-by: Zavian Wang <36817799+Zavianx@users.noreply.github.com>
2026-05-17 07:13:55 +01:00
Vincent Koc
826c2f4517 test(qa-lab): add codex read vocabulary canary 2026-05-17 14:12:50 +08:00
ragesaq
58f1db1bc8 Fix OpenAI Codex runtime provider routing (#82864)
* fix: route Codex OpenAI runtime through Codex provider

* docs: add Codex routing evidence collection

* fix(agents): bootstrap OAuth credentials for Codex harness with openai/* model refs

When a plugin harness (e.g. Codex) owns its transport but the runtime
plan resolved to openai-codex via agentRuntime.id: codex, the auth
profile store was left empty because pluginHarnessOwnsTransport short-
circuited initializeAuthProfile(). This caused 'No API key found for
openai-codex' at runtime even though the OAuth profile existed in OpenClaw's
store.

- Add pluginHarnessNeedsOpenClawAuthBootstrap flag when harness owns
transport but the provider is openai-codex and the API is openai-codex-
responses
- Populate authStore and attemptAuthProfileStore from OpenClaw's profile
store in this case
- Run initializeAuthProfile() to forward the OAuth token into the harness
- Update overflow-compaction tests to expect 'openai-codex' provider
  and add dedicated test for OAuth bootstrap path

* fix(agents): refresh Codex OAuth credentials on profile rotation

---------

Co-authored-by: PsiClawOps <267826480+PsiClawOps@users.noreply.github.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-05-17 07:06:18 +01:00
Peter Steinberger
451563b950 ci: allow Tideclaw alpha release workflows 2026-05-17 07:00:53 +01:00
Vincent Koc
e66a6c8c8d test(qa-lab): add runtime parity depth scenarios 2026-05-17 13:50:18 +08:00
Peter Steinberger
16ef041b5d fix(channels): preserve implicit default accounts 2026-05-17 06:42:28 +01:00
Peter Steinberger
71b79f008d fix: sanitize Codex image payload replay (#82931) 2026-05-17 06:42:21 +01:00
Gio Della-Libera
b7f3d01633 fix(mcp): inline local refs in bundled tool schemas (#81238) 2026-05-16 22:41:11 -07:00
Peter Steinberger
ad155fbbd7 fix(gateway): restore v4 message action protocol 2026-05-17 06:35:39 +01:00
Peter Steinberger
c3e2b3c323 docs: sync plugin generated references 2026-05-17 06:34:58 +01:00
Peter Steinberger
1ceebf8a01 ci: harden release publish evidence 2026-05-17 06:34:58 +01:00
Peter Steinberger
c4d8e0be18 ci: harden release validation flow 2026-05-17 06:34:58 +01:00
Peter Steinberger
a535978352 fix(gateway): explain protocol mismatches 2026-05-17 06:34:04 +01:00
Peter Steinberger
06ec6b0fca fix(mac): speed up channels settings 2026-05-17 06:34:04 +01:00
Peter Steinberger
38b3e73622 fix: improve gateway protocol mismatch diagnostics (#82908)
* fix: improve gateway protocol mismatch diagnostics

* test: cover daemon deep connection diagnostics

* fix: normalize mapped loopback gateway clients
2026-05-17 06:33:34 +01:00
Peter Steinberger
926a5a825f test(channels): avoid catalog scan in command account tests 2026-05-17 06:30:09 +01:00
Gio Della-Libera
9ac7773b7f fix(node): hide Windows task launcher (#81267) 2026-05-16 22:25:33 -07:00
Gio Della-Libera
5817e478d1 fix(agents): clear poisoned claude cli sessions (#81247) 2026-05-16 22:25:09 -07:00
Jesse Merhi
7c2425a518 Support HTTPS managed proxy CA trust (#79171)
* fix: support HTTPS managed proxy CA trust

* fix: strip IP SNI for HTTPS proxy dispatchers

* fix: harden managed proxy undici dispatchers

* docs: refresh proxy baselines

* fix: drop stale whatsapp undici dependency

* fix: satisfy proxy dispatcher lint and tests

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-05-17 06:23:30 +01:00
Peter Steinberger
421b9e2819 fix: restore Codex snapshot tool progress (#82917)
# Conflicts:
#	CHANGELOG.md
2026-05-17 06:20:59 +01:00
Peter Steinberger
3fad770510 fix: update PI runtime packages 2026-05-17 06:12:09 +01:00
Peter Steinberger
6a8a6551fc test(discord): cover durable chunk retry delivery (#82898)
* test(discord): cover durable chunk retry delivery

* test(discord): use plugin sdk test runtime

* fix(telegram): satisfy message cache strict checks

* test(discord): include durable delivery in changed lane
2026-05-17 06:11:58 +01:00
github-actions[bot]
df23b0f86c chore(ui): refresh fa control ui locale 2026-05-17 05:11:38 +00:00
github-actions[bot]
5a350431bd chore(ui): refresh nl control ui locale 2026-05-17 05:11:29 +00:00
github-actions[bot]
cb71ad5a60 chore(ui): refresh vi control ui locale 2026-05-17 05:11:13 +00:00
github-actions[bot]
7899e99852 chore(ui): refresh pl control ui locale 2026-05-17 05:11:05 +00:00
github-actions[bot]
f12b6fa67c chore(ui): refresh th control ui locale 2026-05-17 05:11:00 +00:00
github-actions[bot]
1d2aa4db61 chore(ui): refresh id control ui locale 2026-05-17 05:10:56 +00:00
github-actions[bot]
a16150c7e2 chore(ui): refresh uk control ui locale 2026-05-17 05:10:35 +00:00
github-actions[bot]
09c8f972eb chore(ui): refresh it control ui locale 2026-05-17 05:10:24 +00:00
github-actions[bot]
6c5f97d0d0 chore(ui): refresh tr control ui locale 2026-05-17 05:10:22 +00:00
github-actions[bot]
17109bc253 chore(ui): refresh ar control ui locale 2026-05-17 05:10:15 +00:00
Agustin Rivera
9b96f81327 fix(skills): honor tool policy for inline dispatch (#78525)
* fix(skills): honor tool policy for inline dispatch

* fix(skills): cover inline dispatch policy gaps

* fix(skills): align inline dispatch policy

* fix(skills): add inline dispatch policy changelog
2026-05-16 22:09:53 -07:00
github-actions[bot]
6da6abdb55 chore(ui): refresh es control ui locale 2026-05-17 05:09:50 +00:00
github-actions[bot]
10a0c43872 chore(ui): refresh fr control ui locale 2026-05-17 05:09:48 +00:00
github-actions[bot]
743ad4f296 chore(ui): refresh ja-JP control ui locale 2026-05-17 05:09:45 +00:00
github-actions[bot]
f5d0345feb chore(ui): refresh ko control ui locale 2026-05-17 05:09:40 +00:00
github-actions[bot]
8901cf8625 chore(ui): refresh zh-TW control ui locale 2026-05-17 05:09:13 +00:00
github-actions[bot]
5fddcfaefe chore(ui): refresh pt-BR control ui locale 2026-05-17 05:09:09 +00:00
github-actions[bot]
b7893fc158 chore(ui): refresh de control ui locale 2026-05-17 05:09:05 +00:00
github-actions[bot]
6ca0cd4337 chore(ui): refresh zh-CN control ui locale 2026-05-17 05:09:02 +00:00
Peter Steinberger
84ec0c27bf [codex] Add Control UI sidebar session shortcuts (#82810)
* feat(control-ui): add sidebar session shortcuts

* fix(control-ui): simplify cron job creation

* style(control-ui): pad settings config chrome

* fix(control-ui): fold cron advanced creation into dialog

* fix(control-ui): filter sidebar recent sessions

* fix(control-ui): clean up chat loading state

* fix(control-ui): add settings nav content gutter

* fix(control-ui): inherit settings nav icon color

* fix(control-ui): polish sidebar new session button

* fix(control-ui): speed up communications settings load

* fix(control-ui): add cron strings to locale maps

* fix(control-ui): refresh i18n metadata for sidebar strings

* fix(control-ui): refresh chat raw-copy baseline
2026-05-17 06:07:14 +01:00
hcl
f2d8f38315 fix(followup): route CLI runtime drains through CLI runner (#82847) (#82857)
* fix(followup): route CLI runtime drains through CLI runner

* fix(followup): route queued CLI runtimes

---------

Co-authored-by: hclsys <hclsys@openclaw.ai>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-05-17 06:07:08 +01:00
Josh Avant
422a1374e0 Fix silent success for non-deliverable Bedrock Telegram turns (#82905)
* fix: handle non-deliverable terminal turns

* chore: add changelog for non-deliverable turns

* fix: align telegram message cache types
2026-05-16 23:57:52 -05:00
Ayaan Zaidi
ee10fe17f0 fix(telegram): preserve reply-chain context (#82863) 2026-05-17 10:04:02 +05:30
Ayaan Zaidi
741eafea5f fix(telegram): distinguish partial reply snapshots 2026-05-17 10:04:02 +05:30
Ayaan Zaidi
8880a5827a test(telegram): prove bot reply-chain context 2026-05-17 10:04:02 +05:30
Ayaan Zaidi
440e7d2a87 fix(telegram): preserve reply-chain context 2026-05-17 10:04:02 +05:30
Peter Steinberger
abb06c6e40 fix(gateway): require auth for exposed startup
Co-authored-by: Coy Geek <65363919+coygeek@users.noreply.github.com>
2026-05-17 05:32:26 +01:00
Galin Iliev
18812bfc03 fix(process): clarify lane wait diagnostics (#82792)
Merged via squash.

Prepared head SHA: 1a09b724a5
Co-authored-by: galiniliev <5711535+galiniliev@users.noreply.github.com>
Reviewed-by: @galiniliev
2026-05-16 21:26:31 -07:00
Galin Iliev
150179def7 fix(ui): track gateway protocol constants
Fixes #82882.

Browser Control UI connect frames now advertise the shared Gateway protocol constants instead of stale protocol 4 literals, and the node UI gateway test asserts the emitted protocol range.
2026-05-16 21:25:05 -07:00
Gio Della-Libera
bd51d8f2dd Deduplicate preview-streamed final replies (#82625)
Track the latest partial-preview reply text during reply-agent runs and suppress matching final text-only payloads so Telegram partial streaming does not resend already-previewed blocks when block streaming is disabled.

Keep the dedupe exact-match based to avoid dropping unrelated short finals, preserve errors, and keep unsent media while stripping duplicate caption text.
2026-05-16 21:24:34 -07:00
Gio Della-Libera
5c02b72413 Preserve authored config metadata in doctor (#82687)
* Preserve authored config metadata in doctor

* Preserve legacy default model during doctor repair

Keep defaultModel out of the public schema while allowing doctor repair writes to preserve the legacy root metadata key.
2026-05-16 21:23:32 -07:00
Peter Steinberger
6a1b167472 fix: improve mac settings performance 2026-05-17 05:21:47 +01:00
Josh Avant
7d1317634e fix(agents): use current assistant final payloads (#82850) 2026-05-16 23:20:43 -05:00
Peter Steinberger
b77077f4d1 fix(github-copilot): request identity-encoded API responses 2026-05-17 05:20:06 +01:00
Peter Steinberger
5d81c29cc4 fix: reconcile subagent wait timeouts
Fixes #82787 by keeping session-backed parent subagent runs active when agent.wait only hits a poll timeout before the child session settles. Refactors terminal session-store reconciliation into a shared helper and rejects stale terminal rows from reused child sessions.

Verification:
- CodexReview clean
- pnpm test src/agents/subagent-registry.test.ts src/agents/subagent-registry.lifecycle-retry-grace.e2e.test.ts src/agents/openclaw-tools.subagents.sessions-spawn.lifecycle.test.ts -- --reporter=dot
- git diff --check
- pnpm check:changed via Blacksmith Testbox tbx_01krt1rxpkb7vj53mkaqwfserq
- GitHub CI/CodeQL/OpenGrep/Workflow Sanity green; proof gate covered by maintainer proof: override label
2026-05-17 05:16:36 +01:00
Peter Steinberger
06e85d5eaf fix: honor explicit message tool allowlists (#82889) 2026-05-17 05:11:49 +01:00
Neerav Makwana
2c549ae205 fix(cli): support image describe urls (#82854)
Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-05-17 05:06:27 +01:00
Josh Avant
ab2943e2ff fix(update): repair configured plugins during legacy upgrades (#82859)
* fix(update): repair configured plugins during legacy upgrades

* docs(changelog): note legacy plugin upgrade repair

* test(update): use valid whatsapp upgrade fixture
2026-05-16 22:49:07 -05:00
Galin Iliev
91ae1a6c03 fix(agents): split embedded attempt dispatch timing
Split embedded-run startup diagnostics into attempt-workspace, attempt-prompt, attempt-runtime-plan, and final attempt-dispatch subspans. Adds focused timing formatter coverage and a changelog entry. Fixes #82782.
2026-05-16 20:44:24 -07:00
Alex Knight
9bb4d1377a Fix brew-only skill installs in Docker (#82845)
Summary:
- The branch hides brew-only skill dependency installers during Linux-container onboarding when Homebrew is unavailable, adds container-specific missing-brew guidance, and updates docs, tests, i18n, and changelog text.
- Reproducibility: yes. Current main source inspection shows onboarding can offer a brew-only missing skill su ... ric missing-brew failure; the PR body also includes Testbox container output for before and after behavior.

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

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

Prepared head SHA: a4842f3a7d
Review: https://github.com/openclaw/openclaw/pull/82845#issuecomment-4468958593

Co-authored-by: Alex Knight <15041791+amknight@users.noreply.github.com>
2026-05-17 03:37:51 +00:00
Alex Knight
46fde2bde2 Fix isolated best-effort cron delivery with active subagents (#82843)
Summary:
- The branch gates isolated cron descendant waits and active-descendant delivery suppression on non-best-effort delivery, adds focused regression coverage, and records an unreleased changelog fix.
- Reproducibility: yes. Source inspection on current main shows the best-effort path reaches the full descenda ... nt suppression without checking deliveryBestEffort; the PR body also records before/after Testbox evidence.

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

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

Prepared head SHA: 1a4680126f
Review: https://github.com/openclaw/openclaw/pull/82843#issuecomment-4468954163

Co-authored-by: Alex Knight <15041791+amknight@users.noreply.github.com>
2026-05-17 03:37:31 +00:00
Peter Steinberger
549a0ea313 fix(discord): recover truncated progress finals
Summary:
- Add shared SDK helpers for transcript-backed recovery of ellipsis-truncated final text.
- Use the helper in Discord progress preview delivery so long answers fall through to normal chunked delivery with the full transcript text.
- Refactor Telegram to reuse the shared helper.

Verification:
- node scripts/run-vitest.mjs src/plugin-sdk/channel-streaming.test.ts extensions/discord/src/monitor/message-handler.process.test.ts
- pnpm exec oxfmt --check --threads=1 src/plugin-sdk/channel-streaming.ts src/plugin-sdk/channel-streaming.test.ts extensions/telegram/src/lane-delivery-text-deliverer.ts extensions/telegram/src/lane-delivery.ts extensions/telegram/src/bot-message-dispatch.ts extensions/discord/src/monitor/message-handler.process.ts extensions/discord/src/monitor/message-handler.process.test.ts
- node scripts/run-tsgo.mjs -p test/tsconfig/tsconfig.extensions.test.json --incremental --tsBuildInfoFile .artifacts/tsgo-cache/extensions-test.tsbuildinfo
- git diff --check
- pnpm check:changed via Blacksmith Testbox tbx_01krsy80a5qgfw790nm45770xt
- GitHub PR checks green on #82862
- codex-review --mode local: clean, no accepted/actionable findings

Fixes #82807.
2026-05-17 04:26:35 +01:00
Josh Avant
39a9a3478f Fix heartbeat runner failure copy (#82848)
* fix: scope heartbeat runner failures

* chore: add heartbeat failure changelog
2026-05-16 22:23:22 -05:00
Peter Steinberger
45d9a09485 fix: preserve Signal group session IDs (#82853) 2026-05-17 04:20:51 +01:00
WhatsSkiLL
f562690612 fix(sessions): prune malformed missing transcript rows (#82745)
Fix sessions cleanup pruning for malformed missing transcript rows and preserve metadata-only session rows.

Thanks @IWhatsskill.

Co-authored-by: JARVIS-Glasses <284122573+JARVIS-Glasses@users.noreply.github.com>
2026-05-17 04:19:56 +01:00
Peter Steinberger
b29152e3b9 fix(cron): track claimed reply hooks as execution 2026-05-17 04:12:40 +01:00
Peter Steinberger
b328f57bc3 fix(channels): show missing external channel config (#82849) 2026-05-17 04:10:26 +01:00
Marcus Castro
5040eb5d84 fix: add WhatsApp document fallback extensions (#82851) 2026-05-17 00:09:51 -03:00
Vincent Koc
9b5f5b8651 changelog: add memory startup catch-up and telegram default account preservation 2026-05-17 11:00:37 +08:00
Peter Steinberger
d887eb8dc2 fix(agents): harden subagent completion delivery
Co-authored-by: Galin Iliev <galini@microsoft.com>
Co-authored-by: Ava Daigo <theavadaigo@gmail.com>
Co-authored-by: Moeed Ahmed <moeedahmed@users.noreply.github.com>
2026-05-17 03:48:25 +01:00
Vincent Koc
d801d27dbc fix(qa-lab): add gateway log sentinels 2026-05-17 10:45:14 +08:00
Peter Steinberger
ca236d098d fix: harden gateway launchd and configure sections 2026-05-17 03:44:05 +01:00
Peter Steinberger
524185a68e fix(exec): bind approval trust to realpaths (#82825) 2026-05-17 03:41:50 +01:00
Gio Della-Libera
8af2af24a5 fix(memory): catch up stale sessions on startup (#82341)
* fix(memory): catch up stale sessions on startup

Add a startup catch-up scan for memory session source files so clean gateway restarts compare on-disk transcripts against persisted index file state and mark only missing/newer/resized session files dirty for a normal incremental sync.

* fix(memory): catch up sessions for cli indexing

Ensure one-shot memory index managers also compare session transcripts against persisted source state before no-force CLI syncs, so openclaw memory index can recover stale session rows without requiring --force.

* chore: refresh CI after main repairs
2026-05-16 19:39:55 -07:00
hcl
845da0ed8f fix(gateway): avoid blocking usage cache refreshes (#82778)
- Refresh selected session usage summaries with bounded background work instead of blocking Gateway responses on full transcript scans.
- Persist transcript-level usage metadata so cached full and ranged summaries preserve totals, model usage, tool usage, latency, and time buckets.
- Add regression coverage for background refresh, range derivation, cache-version invalidation, append-only upgrades, and untimestamped usage.

Fixes #82773.

Co-authored-by: hclsys <hclsys@openclaw.ai>
2026-05-17 03:35:15 +01:00
Gio Della-Libera
c4f20b656e fix(telegram): preserve implicit default account (#82794)
Keep the top-level Telegram default account in the account list when named accounts or bindings are added alongside top-level credentials. This preserves default polling while still allowing named-only configs to resolve to their single configured account.
2026-05-16 19:30:52 -07:00
Youssef Hemimy
94ed68bc76 fix(whatsapp): honor forceDocument flag end-to-end (#79272)
Merged via squash.

Prepared head SHA: faaff35f1e
Co-authored-by: itsuzef <53057646+itsuzef@users.noreply.github.com>
Co-authored-by: mcaxtr <7562095+mcaxtr@users.noreply.github.com>
Reviewed-by: @mcaxtr
2026-05-16 23:29:01 -03:00
Peter Steinberger
c1bc6adfaa fix: restore busy TUI draft 2026-05-17 03:26:33 +01:00
Harry Xie
2c7200f542 fix(tui): preserve draft while chat is busy 2026-05-17 03:26:33 +01:00
Peter Steinberger
ab595dec0f fix: normalize malformed assistant replay content (#82748) 2026-05-17 03:26:04 +01:00
IWhatsskill
ad8ae05f37 fix(agents): normalize malformed assistant content 2026-05-17 03:26:04 +01:00
Peter Steinberger
1896f8a330 fix: resolve installed plugin facade dist surfaces 2026-05-17 03:25:42 +01:00
Vincent Koc
c8e12ca01d changelog: add browser MCP redaction and MEMORY/TTS guidance (#81930) 2026-05-17 10:23:47 +08:00
gleb
9f112a1a7a fix: include checked credential source in missing auth errors
Include the checked credential source in missing API key errors so users can see which env var, profile, or config path to fix.

Fixes #82785.

Co-authored-by: gleb <116607327+loeclos@users.noreply.github.com>
2026-05-17 03:21:57 +01:00
Gio Della-Libera
6821fbcfba Clarify MEMORY guidance over generic TTS hints (#81930) 2026-05-16 19:21:24 -07:00
Peter Steinberger
9e67f53b91 fix(cli): resolve web command SecretRefs
Fix CLI web search/fetch command SecretRef resolution for provider-scoped plugin credentials.

- Carry command provider overrides through gateway and local secret resolution.
- Mark the selected web provider targets active and unrelated plugin refs inactive.
- Cover Tavily, DuckDuckGo, legacy Firecrawl fetch, protocol overrides, and runtime command-secret behavior.
- Add public plugin-sdk test mock exports needed by existing plugin tests after CI boundary enforcement.

Fixes #82621.
Replacement for #82699.

Co-authored-by: 吴杨帆 <39647285+leno23@users.noreply.github.com>
2026-05-17 03:00:39 +01:00
Vincent Koc
ecb9028f9f fix(browser): redact chrome mcp attach details 2026-05-17 09:53:18 +08:00
Vincent Koc
55e4b76bb2 fix(browser): preserve raw chrome launch diagnostics 2026-05-17 09:53:18 +08:00
Vincent Koc
82e8b5232d fix(browser): redact chrome bridge diagnostics 2026-05-17 09:53:17 +08:00
Vincent Koc
d183fa3095 changelog: note Copilot replay item-id collision fix 2026-05-17 09:52:35 +08:00
Vincent Koc
d62e443b36 fix(plugins): honor parent bundled source overlays 2026-05-17 09:52:19 +08:00
Vincent Koc
817f0cd6c8 fix(plugins): preserve bundled source overlay probes 2026-05-17 09:52:19 +08:00
Vincent Koc
c8b5757303 fix(plugins): try runtime package-state probes before source 2026-05-17 09:52:19 +08:00
Vincent Koc
634a766347 docs(changelog): note bundled plugin path hardening 2026-05-17 09:52:19 +08:00
Vincent Koc
662fcb81c9 fix(plugins): align bundled runtime surface roots 2026-05-17 09:52:19 +08:00
Vincent Koc
77f7c8df8d fix(plugins): contain bundled entry paths 2026-05-17 09:52:19 +08:00
Peter Steinberger
2a7f9f3546 fix: avoid Copilot replay item ID collisions 2026-05-17 02:43:44 +01:00
Peter Steinberger
f2a0b3d2e2 chore(crabbox): warn about raw aws runtime commands 2026-05-17 02:38:10 +01:00
Vincent Koc
d350ac3feb test: use platform spy helpers 2026-05-17 09:24:42 +08:00
Peter Steinberger
7ee5fe011b refactor(agents): share model manifest context 2026-05-17 02:24:07 +01:00
Vincent Koc
da8afe359d feat(qa-lab): add scenario pack selector 2026-05-17 09:23:48 +08:00
Peter Steinberger
dcb4160909 docs: clarify Crabbox scenario proof 2026-05-17 02:23:12 +01:00
Galin Iliev
4537b89da6 fix(agents): normalize Copilot replay tool IDs
Normalize GitHub Copilot Responses replay tool-call IDs before dispatch so resumed sessions with historical overlong item IDs no longer fail Copilot schema validation.

Closes #82749.
2026-05-16 18:22:10 -07:00
Josh Avant
e50927b6c9 fix(telegram): keep streamed text when tts arrives (#82820)
* fix(telegram): keep streamed text when tts arrives

* docs(changelog): note telegram tts stream fix
2026-05-16 20:20:05 -05:00
Peter Steinberger
ffc7bda443 fix(qwen): honor chat-template thinking level 2026-05-17 02:19:26 +01:00
Peter Steinberger
f453904165 feat: add fal and OpenRouter music generation (#82789)
* feat: add fal and OpenRouter music generation

* fix: repair music generation CI gates

* chore: refresh proof gate
2026-05-17 02:05:22 +01:00
Josh Avant
562d460d75 fix(codex): guard post-tool raw assistant terminal gaps (#82816)
* fix(codex): guard post-tool raw assistant terminal gaps

* docs(changelog): note codex terminal guard fix
2026-05-16 20:04:39 -05:00
Peter Steinberger
a6225060f1 fix(memory): abort timed-out embedding requests (#82770)
* fix(memory): abort timed-out embedding requests

* test: stabilize gateway ci shards

* test: pin control ui origin fixture

* test: stabilize gateway ci fixtures

* test: isolate forged origin fixture

* test: decouple setup code from gateway net mocks

* test: repair run-node and config preaction CI

* test: fix run-node progress fixture typing

* test: remove unused pairing setup helper

* fix: stabilize embedding timeout errors
2026-05-17 02:04:17 +01:00
Vincent Koc
b77b3a7ade changelog: add Anthropic-messages reasoning_content thinking-block extraction 2026-05-17 09:02:35 +08:00
Vincent Koc
54c9820ed9 test(agents): cover anthropic reasoning replay 2026-05-17 08:58:52 +08:00
Sunnyone2three
5fe4e09b68 fix: extract reasoning_content from thinking blocks instead of non-existent top-level field 2026-05-17 08:58:52 +08:00
Sunnyone2three
8e21b3c9a6 fix: preserve reasoning_content in anthropic-messages transport for proxy providers 2026-05-17 08:58:52 +08:00
Vincent Koc
e0c3c80ebc test: share Windows platform spy helpers 2026-05-17 08:56:56 +08:00
Josh Avant
2416de1421 Fix infer SecretRef resolution for provider-backed commands (#82798)
* fix infer secretref resolution

* chore changelog for infer secretrefs
2026-05-16 19:55:39 -05:00
Shakker
f5904392e9 fix: scope plugin metadata to workspace context 2026-05-17 01:55:16 +01:00
Shakker
193bfd3a4d fix: avoid stale workspace metadata reuse 2026-05-17 01:55:16 +01:00
Shakker
bfceb0d7f9 fix: keep metadata reuse scoped to agent turns 2026-05-17 01:55:16 +01:00
Shakker
121cd054ef fix: reuse plugin metadata in local agent turns 2026-05-17 01:55:16 +01:00
Shakker
c90e42aaa7 fix: reuse plugin metadata during runtime loading 2026-05-17 01:55:16 +01:00
Shakker
20704ffab7 fix: reuse manifest metadata in model selection 2026-05-17 01:55:16 +01:00
Josh Avant
fba250d4a2 Fix Discord reply context at LLM boundary (#82801)
* fix: preserve current reply context

* docs: add reply context changelog
2026-05-16 19:54:24 -05:00
Vincent Koc
5911b5bf2d fix(qa-lab): stabilize mock qa-channel regressions 2026-05-17 08:50:31 +08:00
Peter Steinberger
0fdc280cdd fix(codex): keep native hook relay config final
Co-authored-by: Solomon Neas <me@solomonneas.dev>
2026-05-17 01:49:56 +01:00
Galin Iliev
7c151b212b fix(agents): prioritize manual session turns (#82765)
* fix(agents): prioritize manual session turns

* docs: update changelog for session priority

---------

Co-authored-by: Galin Iliev <Galin.Iliev@microsoft.com>
2026-05-16 17:49:48 -07:00
Josh Avant
ad450a7dfb Fix Windows image model event loop stalls (#82799)
* fix media image model event loop stalls

* changelog for image event loop fix

* fix async auth test mock
2026-05-16 19:49:11 -05:00
Peter Steinberger
89532d3a92 fix(codex): satisfy shared-client state typing 2026-05-17 01:46:39 +01:00
Peter Steinberger
c6ffacd1db fix(codex): surface app-server close failures 2026-05-17 01:46:39 +01:00
Peter Steinberger
191bd7dc9a fix(codex): scope app-server migration cleanup 2026-05-17 01:46:39 +01:00
Peter Steinberger
b30face031 fix(codex): migrate legacy app-server state 2026-05-17 01:46:39 +01:00
Peter Steinberger
4cbf616d30 fix(codex): premark terminal app-server turns 2026-05-17 01:46:39 +01:00
Peter Steinberger
c65801c5a9 fix(codex): preserve completed app-server turns 2026-05-17 01:46:39 +01:00
Peter Steinberger
06b902e33f fix(codex): abort work when app-server closes 2026-05-17 01:46:39 +01:00
Peter Steinberger
ea4ee23fa2 chore: keep codex changelog scoped 2026-05-17 01:46:39 +01:00
Peter Steinberger
84d3b7a389 fix(codex): isolate shared app-server clients 2026-05-17 01:46:39 +01:00
Peter Steinberger
80848fc040 fix(config): accept gateway remote port 2026-05-17 01:41:17 +01:00
Peter Steinberger
e98ebb5739 fix(ci): format macOS Swift sources 2026-05-17 01:36:12 +01:00
Peter Steinberger
f0513221d7 fix: improve mac menu status errors 2026-05-17 01:35:20 +01:00
Peter Steinberger
a70459d10b fix: keep telegram floor replays dispatchable 2026-05-17 01:34:54 +01:00
Peter Steinberger
b09e11bc69 fix: harden telegram routing edge cases 2026-05-17 01:34:54 +01:00
Peter Steinberger
983e8d39da docs: add telegram routing changelog 2026-05-17 01:34:54 +01:00
Miya
5239b20089 fix telegram polling and message action scopes 2026-05-17 01:34:54 +01:00
Josh Avant
8d3027dffa Remove OAuth sidecar credential runtime support (#82777)
* fix(auth): remove oauth sidecar runtime support

* docs(changelog): note oauth sidecar removal
2026-05-16 19:33:30 -05:00
Ayaan Zaidi
d1280a3de9 fix(discord): preserve room event history until delivery (#82573)
Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-05-17 01:24:33 +01:00
Peter Steinberger
d7ad12dde4 test(cli): force progress test out of CI mode 2026-05-17 01:13:24 +01:00
Peter Steinberger
e19f05b79b test(cli): repair startup progress checks 2026-05-17 01:13:24 +01:00
Peter Steinberger
97abe0f0c0 fix(gateway): harden origin and pairing checks 2026-05-17 01:13:24 +01:00
Peter Steinberger
388b7456d2 docs: add telegram captionless media changelog (#82756) 2026-05-17 01:13:24 +01:00
JARVIS-Glasses
2c4287b6df fix(telegram): honor catch-all mentions for captionless media 2026-05-17 01:13:24 +01:00
Peter Steinberger
ff8d3dc591 fix: improve mac pairing approval prompt 2026-05-17 01:12:59 +01:00
Peter Steinberger
c2e90914b7 fix: simplify talk options panel 2026-05-17 00:42:54 +01:00
Peter Steinberger
12debcb05e fix(cli): improve config startup progress 2026-05-17 00:37:39 +01:00
Peter Steinberger
c528f36507 fix(feishu): reject numeric wiki space ids (#82769) (thanks @hyspacex) 2026-05-17 00:34:58 +01:00
Harry Xie
3411a481f7 fix(feishu): reject numeric wiki space ids 2026-05-17 00:34:58 +01:00
Peter Steinberger
32f7481787 docs: route long coding agent builds through issues 2026-05-17 00:15:02 +01:00
吴杨帆
cc9117f729 fix: validate limit edge cases and voicecall numeric flags (#82679)
Fix diagnostics/session usage limit handling and voice-call numeric CLI validation.

- Treat explicit zero, negative, and non-finite diagnostics/session limits as empty results instead of falling back to defaults.
- Reject invalid, non-finite, and fractional voice-call numeric flags.
- Add focused tests and a live repro proof for the canonical edge cases.

Fixes #82646, #82650, #82651, #82653.

Co-authored-by: wuyangfan <1102042793@qq.com>
2026-05-17 00:11:46 +01:00
Josh Avant
99a269f8b4 fix(agents): mark interrupted sessions before restart (#82772)
* fix(agents): mark interrupted sessions before restart

* docs: add restart recovery changelog

* fix(agents): satisfy restart recovery type checks
2026-05-16 18:11:35 -05:00
Peter Steinberger
0190f4ae1e fix: finish inbound event rebase (#82606) 2026-05-17 00:10:29 +01:00
Peter Steinberger
d0efaceb97 fix: keep room events quiet across legacy helpers 2026-05-17 00:10:29 +01:00
Peter Steinberger
cdf8121a04 chore: refresh plugin sdk api baseline 2026-05-17 00:10:29 +01:00
Peter Steinberger
1d22578c6c chore: drop generated artifacts from refactor branch 2026-05-17 00:10:29 +01:00
Peter Steinberger
07f05e972e refactor: move inbound event classification into core 2026-05-17 00:10:29 +01:00
XING
6b4d371723 fix(secrets): treat env refs as audit-safe auth values
Fix secrets audit env-ref classification and document supported auth SecretRef shorthand.\n\nCo-authored-by: XING <wxinxings@gmail.com>
2026-05-17 00:05:10 +01:00
Gio Della-Libera
3b2cd0dd1a Honor cwd for native subagent spawns (#81896)
* Honor cwd for native subagent spawns

Thread sessions_spawn cwd through the native subagent path, use the resolved child workspace for attachment materialization, and keep workspace metadata internal to the gateway boundary.

* Refresh checks after proof update
2026-05-16 15:56:13 -07:00
Peter Steinberger
d533a65f56 fix: default music generation timeout to five minutes 2026-05-16 23:50:58 +01:00
Peter Steinberger
5b383af736 feat: add native mac dashboard window 2026-05-16 23:49:18 +01:00
100menotu001
21244d9793 fix(tasks): make delegated completions review-ready
Co-authored-by: Craig <froelich@craigs.mac.studio.froho>
2026-05-16 23:47:47 +01:00
Gio Della-Libera
a136cafe98 Default bootstrap truncation warnings to always (#81918)
* Default bootstrap truncation warnings to always

Make bootstrap truncation warnings surface on every affected run by default while preserving explicit off and once configuration.

* Refresh checks after proof formatting fix

* Refresh checks after live proof update

* docs: align bootstrap warning default reference

Update the public agent config reference to match the new default bootstrapPromptTruncationWarning mode and recommended example.
2026-05-16 15:46:44 -07:00
Peter Steinberger
bea4f0d2f4 fix(gateway): defer heartbeats during active replies
* fix(gateway): defer heartbeats during active replies

* fix(gateway): bind heartbeat reply run fallback
2026-05-16 23:43:52 +01:00
Peter Steinberger
77ca3dc99c fix: allow direct media failure summaries 2026-05-16 23:41:48 +01:00
Peter Steinberger
7abae15a6b fix: keep music generation timeouts internal 2026-05-16 23:41:48 +01:00
Peter Steinberger
6e2e63a983 fix: suppress equivalent OpenAI Codex fallback notices 2026-05-16 23:41:48 +01:00
Vincent Koc
8bef5d0d62 fix(qa-lab): stabilize threaded memory parity 2026-05-17 06:41:21 +08:00
Peter Steinberger
41777fb0fa fix(cli): prioritize auth provider picker 2026-05-16 23:40:49 +01:00
Peter Steinberger
ac2a1e5b50 fix(minimax): declare CN provider auth aliases
Co-authored-by: kamusis <kamusis@gmail.com>
2026-05-16 23:37:03 +01:00
Josh Avant
045a581069 fix(sandbox): honor explicit docker env (#82763)
* fix(sandbox): honor explicit docker env

* docs(changelog): note sandbox env fix
2026-05-16 17:36:05 -05:00
Peter Steinberger
54619d4033 fix(discord): keep progress drafts for tool-only replies 2026-05-16 23:29:55 +01:00
Vincent Koc
81a578fd6b fix(qa-lab): validate capture saved views 2026-05-17 06:28:57 +08:00
Peter Steinberger
36e88f5ddd docs: clarify file-backed secret refs 2026-05-16 23:28:39 +01:00
Peter Steinberger
ac99494e44 test: tighten session_status run-session proof (#82696) 2026-05-16 23:27:19 +01:00
wuyangfan
361dc69029 chore: add live repro for session_status runSessionKey proof
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-16 23:27:19 +01:00
wuyangfan
75a9293bf9 fix(session_status): prefer runSessionKey for implicit no-arg lookups
No-arg session_status calls now resolve against the live run session when
runSessionKey is available, so thinking level and other session state match
the active run instead of a stale sandbox/policy key.

Fixes openclaw/openclaw#82669

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-16 23:27:19 +01:00
Peter Steinberger
c1c67306fd fix(openai): restore Codex xhigh thinking metadata (#82761) 2026-05-16 23:25:21 +01:00
Gio Della-Libera
82ab8a8785 fix(config): materialize subagent archive default (#81998) 2026-05-16 15:19:36 -07:00
Peter Steinberger
7b38ac9749 fix(channels): contain draft stream flush failures (#82713)
Co-authored-by: Coy Geek <65363919+coygeek@users.noreply.github.com>
Co-authored-by: opencode <opencode@users.noreply.github.com>
2026-05-16 23:12:39 +01:00
吴杨帆
9791957cd5 fix(qqbot): treat false-like QQBOT_DEBUG values as disabled (#82697)
Fix QQBot debug logging so only explicit truthy `QQBOT_DEBUG` values (`1`, `true`, `yes`, `on`) enable debug output. False-like values such as `0`, `false`, `off`, and `no` now keep debug logs disabled, preventing accidental message-text logging.

Also add the release changelog entry and remove a stale unused daemon inspection helper that failed current `tsgo:prod` after rebasing onto latest main.

Fixes #82644.
Thanks @leno23.

Co-authored-by: wuyangfan <1102042793@qq.com>
2026-05-16 23:10:09 +01:00
Peter Steinberger
93bc99460e fix(daemon): remove unused service marker helper 2026-05-16 23:06:12 +01:00
100menotu001
a1d0b2709a Add security audit suppressions (#76949)
* Add security audit suppressions

* docs: list audit suppression dangerous flag

* fix(security): keep audit suppressions visible

* docs(changelog): thank audit suppression contributor

---------

Co-authored-by: Craig <froelich@craigs.mac.studio.froho>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-05-16 22:57:04 +01:00
Peter Steinberger
3536c927da fix(doctor): avoid launchagent false positives 2026-05-16 22:56:48 +01:00
Gio Della-Libera
2762d9abbe fix(agents): allow workspace-only reads for skills (#82397) 2026-05-16 14:54:19 -07:00
Peter Steinberger
37a1ab0c0b fix: document image generation timeout defaults (#75337) 2026-05-16 22:53:07 +01:00
Peter Steinberger
e485640da4 fix: raise hosted image generation timeouts 2026-05-16 22:53:07 +01:00
Peter Steinberger
fd8afc1dce refactor: unify async media generation
Summary:
- Refactor image/music/video generation onto the shared async media-generation scheduler and task lifecycle.
- Make session-backed image generation async with status, duplicate guarding, active-task prompt context, and message-tool completion delivery.
- Update docs/changelog and add /tasks coverage for image-generation task visibility.

Verification:
- Codex review: no accepted/actionable findings.
- pnpm test src/auto-reply/reply/commands-tasks.test.ts src/agents/tools/image-generate-tool.test.ts src/agents/tools/image-generate-background.test.ts src/agents/image-generation-task-status.test.ts -- --reporter=dot
- Previous focused media suite: 12 files / 169 tests passed.
- Crabbox aws check:changed run run_fbd1b62c7472 passed.
- Crabbox aws live openclaw infer run run_c17929e0e224 passed with OpenAI gpt-image-2.
- GitHub CI for rebased head 74d1cda6a6 completed with no non-success code gates.
2026-05-16 22:50:06 +01:00
Peter Steinberger
fff8e79afb docs: credit subagent handoff PR (#82724) 2026-05-16 22:48:19 +01:00
OpenClaw Contributor
8040f28bc5 fix(subagents): make completion handoff review-first 2026-05-16 22:48:19 +01:00
Vincent Koc
25090f64b3 changelog: add edit-tool file_path alias recovery (#81909) 2026-05-17 05:43:20 +08:00
Gio Della-Libera
9e31b9d344 Fix edit recovery for file_path workspace edits (#81909)
* Recover edit tool failures for file_path

Honor file_path and related aliases when resolving edit recovery paths so post-write errors do not surface false edit failures after the file changed.

* Refresh checks after proof formatting fix

* Refresh checks after live proof update
2026-05-16 14:40:44 -07:00
Zennn
91f45d9c8a fix(gateway): dedupe exec followup continuations (#82717)
Co-authored-by: Miya <miya@Miyas-Mac-mini.local>
2026-05-16 22:39:26 +01:00
Peter Steinberger
842e6f1643 fix(slack): preserve assistant thread context 2026-05-16 22:31:22 +01:00
Peter Steinberger
80eeb688c1 feat(slack): add assistant thread lifecycle 2026-05-16 22:31:22 +01:00
Peter Steinberger
c4fb12ee8d fix: preserve xAI Grok 4.3 default reasoning (#81227) 2026-05-16 22:25:43 +01:00
Jason O'Neal
4f886e7334 fix(xai): gate reasoning effort to supported models 2026-05-16 22:25:43 +01:00
Marcus Castro
6e70d9e4b6 fix: accept WhatsApp group-prefixed targets (#82738) 2026-05-16 18:18:18 -03:00
Peter Steinberger
e9d283e13a fix(setup): summarize gateway config 2026-05-16 22:16:32 +01:00
Peter Steinberger
3934849550 fix: document OpenRouter thinking replay fix (#82380) 2026-05-16 22:14:57 +01:00
hclsys
9fa78718c7 fix(openai-completions): skip non-JSON thinkingSignature provenance tags
The openai-completions OpenRouter passthrough records the response field
name ("reasoning", "reasoning_details", "reasoning_content",
"reasoning_text", "content") as the assistant block's
`thinkingSignature`. Those values are provenance tags rather than
JSON-encoded reasoning items, so replaying them in the next request body
breaks providers that expect a structured signature (OpenRouter returns
HTTP 500 on the 2nd turn for Anthropic Claude and xAI Grok via the
openai-completions API).

Stop persisting the provenance tag (only keep replayable JSON signatures)
and harden the responses replay path with a matching JSON guard so
existing transcripts with poisoned signatures recover cleanly.

Fixes #82335
2026-05-16 22:14:57 +01:00
Peter Steinberger
4b0f16d496 fix(agents): announce auto model fallback transitions (#82676)
* fix(agents): announce model fallback transitions

* docs(agents): explain model fallback notices

# Conflicts:
#	docs/concepts/model-failover.md

* fix(agents): use five minute fallback probe cadence

* fix(agents): keep fallback notices out of ACP transcripts
2026-05-16 21:56:31 +01:00
Peter Steinberger
66c64a29ee fix(gateway): capture opt-in memory pressure snapshots (#82674)
* fix(gateway): persist critical memory pressure bundles

* docs(gateway): add memory pressure troubleshooting

* feat(gateway): gate memory pressure bundles

* feat(gateway): flatten memory pressure bundle config

* feat(gateway): rename memory pressure snapshot config

* fix(gateway): make memory pressure snapshots opt in

* docs(config): refresh config baseline

* fix(config): simplify memory pressure migration default
2026-05-16 21:52:09 +01:00
Peter Steinberger
532e42213d fix: preserve bundle activation metadata (#75133)
Co-authored-by: 100menotu001 <64228916+100menotu001@users.noreply.github.com>
2026-05-16 21:31:56 +01:00
Agustin Rivera
f7977fb102 fix(gateway): reject malformed request targets (#82686)
* fix(gateway): reject malformed request targets

* fix(gateway): document malformed request target rejection
2026-05-16 13:25:49 -07:00
Vincent Koc
55edadf86f fix(qa-lab): ignore heartbeat parity transcripts 2026-05-17 04:24:17 +08:00
Peter Steinberger
6369bf64cd fix(gateway): trace restart intent reasons 2026-05-16 21:23:06 +01:00
hcl
c421be6c90 fix(docs): use lowercase MCP search tool (#82704)
Fixes #82702.

Summary:
- Use the canonical lowercase docs MCP search tool name.
- Keep docs and changelog aligned for the CLI fix.

Verification:
- node scripts/run-vitest.mjs src/commands/docs.test.ts
- pnpm lint -- src/commands/docs.ts src/commands/docs.test.ts
- pnpm exec oxfmt --check CHANGELOG.md docs/cli/docs.md src/commands/docs.ts src/commands/docs.test.ts
- pnpm docs:list
- git diff --check
- HOME=$(mktemp -d) pnpm openclaw docs "browser existing-session"
- Codex review local + branch: clean
- GitHub CI 25971835163, CodeQL Critical Quality 25971835154, Real behavior proof 25971834239: green

Co-authored-by: hclsys <hclsys@users.noreply.github.com>
2026-05-16 21:21:58 +01:00
Vincent Koc
640735cebe fix(qa): serialize runtime parity cells 2026-05-17 04:19:05 +08:00
Peter Steinberger
6d844c5900 fix(webchat): trust ACP TTS media tails (#82701) (thanks @leno23) 2026-05-16 21:18:13 +01:00
wuyangfan
35e1c7ac41 fix(webchat): keep trustedLocalMedia internal to reply payloads
Restore Omit on public plugin-sdk ReplyPayload; set trustedLocalMedia via
runtime assertion in speech-core and explicitly on dispatch TTS-only finals.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-16 21:18:13 +01:00
wuyangfan
eec18fccb4 fix(webchat): forward trustedLocalMedia on accumulated block TTS tail
Avoid per-block final-mode synthesis (duplicate with dispatch tail). Mark
TTS output as trusted local media and pass the flag through the TTS-only
final payload WebChat consumes after block streaming.

Fixes #82628

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-16 21:18:13 +01:00
wuyangfan
f8323f8636 chore: add live repro script for WebChat auto-TTS proof
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-16 21:18:13 +01:00
wuyangfan
14117c303d fix(webchat): enable auto-TTS for block replies with trusted local media
WebChat streaming uses kind=block for assistant text; final-mode TTS skipped
those payloads. Mark synthesized audio as trustedLocalMedia and export the
full ReplyPayload type so the gateway can serve local TTS files.

Fixes #82628

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-16 21:18:13 +01:00
KateWilkins
03012ac5a1 fix: update xai image generation model (#81399)
Updates the xAI image model catalog and docs to use `grok-imagine-image-quality` after `grok-imagine-image-pro` retirement.

Co-authored-by: Kate <kate@trantor.dev>
2026-05-16 21:09:21 +01:00
Peter Steinberger
cc8c0d4ecb fix(config): quiet config write output 2026-05-16 21:08:36 +01:00
Manzojunior
a9e0a897a1 fix: handle xai video pending status (#82610)
Treats xAI video `pending` poll status as in-flight processing and keeps polling until `done`.

Co-authored-by: Chase Young <manzo0924@gmail.com>
2026-05-16 21:03:34 +01:00
Vincent Koc
5db30ab47d fix(extensions): satisfy runtime boundary checks 2026-05-17 03:48:46 +08:00
Vincent Koc
6a12134489 test(release): tolerate package validation drift 2026-05-17 03:33:54 +08:00
Vincent Koc
f345b54d04 test(qa-lab): add runtime parity axis 2026-05-17 03:32:50 +08:00
Peter Steinberger
6e4cc222cb fix(xai): refresh oauth and model catalog 2026-05-16 20:25:07 +01:00
Vincent Koc
7d09ff89ee fix(gateway): honor env token for remote interactive auth 2026-05-17 03:15:54 +08:00
Vincent Koc
ca1fd1b140 test: share spy lifecycle helpers 2026-05-17 03:13:46 +08:00
Peter Steinberger
1a956b6ba1 fix: require message tool for generated media completions 2026-05-16 20:12:42 +01:00
Vincent Koc
df3f983d96 fix(ci): keep unauthorized Mantis commands neutral 2026-05-17 03:11:45 +08:00
Peter Steinberger
c8782d18eb fix(agents): probe primary after auto fallback pin (#82707) 2026-05-16 20:03:09 +01:00
Peter Steinberger
500d282340 docs: record memory-core dreaming cron cleanup (#82389) 2026-05-16 19:58:01 +01:00
Neerav Makwana
ccdcdc7d1b fix(memory-core): retry disabled dreaming cron cleanup 2026-05-16 19:58:01 +01:00
Vincent Koc
440333125c test(qa-lab): add personal agent scenarios 2026-05-17 02:56:53 +08:00
Vincent Koc
1586085c7f test: share node eval helpers 2026-05-17 02:51:20 +08:00
Vincent Koc
11745de9d9 test(e2e): wait for launcher child metadata 2026-05-17 02:48:35 +08:00
Vincent Koc
db4ce1f506 changelog: add config unset --dry-run (#81895) 2026-05-17 02:42:36 +08:00
Vincent Koc
e1061a8b46 test(live): tolerate provider drift in release checks 2026-05-17 02:36:48 +08:00
Vincent Koc
a171600d1d test: isolate broad unit state 2026-05-17 02:32:58 +08:00
Vincent Koc
b19b7539a8 test: fix Codex live Docker api key permissions 2026-05-17 02:32:58 +08:00
Vincent Koc
b6b33ad6d3 test: harden broad qa timing 2026-05-17 02:32:57 +08:00
Vincent Koc
3a13d1e0be test: bind Codex live API key lane through OpenAI 2026-05-17 02:32:57 +08:00
Vincent Koc
f0105939bf test: pass Codex API key into Docker bind lane 2026-05-17 02:32:57 +08:00
Vincent Koc
11a31e476b test: align Codex bind live model 2026-05-17 02:32:57 +08:00
Vincent Koc
3df6499fb8 test: harden sparse Testbox scans 2026-05-17 02:32:57 +08:00
Vincent Koc
09db0892dd test: tolerate sparse Testbox file scans 2026-05-17 02:32:34 +08:00
Vincent Koc
8330582493 test: repair broad qa surface regressions 2026-05-17 02:32:14 +08:00
Vincent Koc
b5b193076e test: share repo file helpers 2026-05-17 02:29:55 +08:00
Vincent Koc
ffd8fcd598 docs(crabbox): note explicit macOS runners 2026-05-17 02:26:51 +08:00
Peter Steinberger
8178a6c949 feat: show provider quota in control ui overview (#82647)
* feat: show provider quota in control ui overview

* feat: show provider quota in chat header

* fix: recover stale control ui chat runs

* fix: polish control ui quota refresh
2026-05-16 19:24:02 +01:00
Vincent Koc
0b03b902be Merge branch 'main' of https://github.com/openclaw/openclaw
* 'main' of https://github.com/openclaw/openclaw:
  test(agents): tolerate Anthropic cache tool drift
2026-05-17 02:23:24 +08:00
Vincent Koc
ac2e3a23b9 fix(qa): preserve RTT samples with Convex credentials 2026-05-17 02:17:35 +08:00
Vincent Koc
ec38e96884 test(agents): tolerate Anthropic cache tool drift 2026-05-17 02:15:11 +08:00
Vincent Koc
d5035bad62 fix(google): keep auth fallback logs quiet 2026-05-17 02:10:11 +08:00
Jason O'Neal
e8b4003933 fix(google): keep first login identity errors 2026-05-17 02:10:11 +08:00
Jason O'Neal
995c702b07 fix(google): wrap Gemini CLI refresh credentials
# Conflicts:
#	CHANGELOG.md
2026-05-17 02:10:11 +08:00
Jason O'Neal
b34454f5b3 fix(google): refresh Gemini CLI OAuth tokens 2026-05-17 02:10:11 +08:00
Gio Della-Libera
489cab2738 fix(config): add --dry-run support to unset (#81895)
* Add config unset dry-run

Add --dry-run support to config unset, including JSON output and allow-exec validation parity with config set/patch dry-run handling.

* Refresh checks after proof update

* fix(config): address unset dry-run review

Return structured JSON when config unset dry-run misses a path and validate broad secret provider/default unsets against affected SecretRefs.
2026-05-16 11:09:42 -07:00
Vincent Koc
e06782d5e7 fix(gateway): land linked diagnostics fixes
Fix logs.tail credential-header redaction and JSON-mode gateway transport errors.\n\nFixes #66832.\nFixes #79108.\nSupersedes #67041.\nSupersedes #79233.\n\nCo-authored-by: Mil Wang <mingjwan@microsoft.com>\nCo-authored-by: Andy Ye <35905412+TurboTheTurtle@users.noreply.github.com>
2026-05-17 02:05:02 +08:00
Peter Steinberger
d77c4bbb2d fix(gateway): harden startup restart queue (#82660) (thanks @samzong) 2026-05-16 18:57:58 +01:00
samzong
9b53a95d8e fix(gateway): queue startup restart signals
Signed-off-by: samzong <samzong.lu@gmail.com>
2026-05-16 18:57:58 +01:00
Vincent Koc
cd1846a313 test(agents): fix embedded runner test config types 2026-05-17 01:56:58 +08:00
Vincent Koc
df9f29caef test(agents): stabilize embedded runner release checks 2026-05-17 01:54:36 +08:00
Vincent Koc
ffcbb89b7e changelog: add gateway diagnostics redaction and Telegram #81229 2026-05-17 01:42:53 +08:00
Vincent Koc
05123db93c fix(agents): redact overlapping auth secrets 2026-05-17 01:42:19 +08:00
Vincent Koc
c818a9fb4e fix(agents): redact oauth refresh errors 2026-05-17 01:42:19 +08:00
Vincent Koc
43c53174c5 fix(agents): harden spawn cleanup and patch paths 2026-05-17 01:42:19 +08:00
Vincent Koc
cb313d5378 test: share fs scan assertions 2026-05-17 01:35:39 +08:00
Vincent Koc
c277138959 test(plugins): share archive fixture packing 2026-05-17 01:35:39 +08:00
Gio Della-Libera
4003a955ee fix(telegram): normalize announce group targets (#81229) 2026-05-16 10:32:58 -07:00
Vincent Koc
61ee9755ad fix(update): preserve channel config across package repair
Preserve channel config across package-swap doctor and post-core repair.\n\nFixes #82533.
2026-05-17 01:32:37 +08:00
Vincent Koc
50508b1d0c fix(gateway): redact credential-bearing diagnostics
Redact credential-bearing gateway target URLs and client diagnostics while preserving raw connection URLs for programmatic use.

Verification:
- node scripts/run-vitest.mjs src/gateway/client.test.ts -- --reporter=verbose -t "connect failure logs"
- node scripts/run-vitest.mjs src/gateway/call.test.ts src/gateway/client.test.ts -- --reporter=dot
- git diff --check
- Testbox check:changed tbx_01krrwjvepsj3458ybk6bk1k6j https://github.com/openclaw/openclaw/actions/runs/25968066889
- codex review --base origin/main
2026-05-17 01:30:55 +08:00
Gio Della-Libera
f22c26a6cd Fix chat session picker agent switching (#81858)
* Fix chat session picker agent switching

Reset the chat session picker to the selected agent main session when switching agents and hide inactive sub-agent sessions from the normal picker options.

* fix(ui): preserve dashboard session on agent switch

Choose the most recent eligible normal/dashboard session for the selected agent while excluding subagent/internal rows; fall back to main only when no eligible session exists.

* fix(ui): avoid mutating session option sort
2026-05-16 10:25:15 -07:00
Vincent Koc
ba103c56a2 changelog: add #82225 Discord identify and #82237 ACP runtime handle refresh 2026-05-17 01:22:49 +08:00
Gio Della-Libera
37cd82913f fix(discord): bind delayed identify to socket generation (#82225)
* fix(discord): bind delayed identify to socket generation
* chore: refresh CI after main repairs
2026-05-16 10:19:35 -07:00
Peter Steinberger
97d1f5fd15 fix: bypass npm freshness filters during updates
Bypass npm min-release-age/before quarantine for OpenClaw-managed package installs and update installer scripts/tests/docs.\n\nFixes #82630.
2026-05-16 18:17:18 +01:00
Vincent Koc
d13749b2fc test(codex): keep dynamic tool helper tests fast 2026-05-17 01:12:20 +08:00
Gio Della-Libera
2640244d35 fix(acp): refresh runtime handles on config changes (#82237)
* fix(acp): refresh runtime handles on config changes
* chore: refresh CI after main repairs
2026-05-16 10:09:36 -07:00
Vincent Koc
28fdc34543 changelog: rewrite #81386 bullet from commit-style to user-facing prose 2026-05-17 01:02:25 +08:00
Pavan Kumar Gondhi
6a12c6f799 fix(gateway): scope session data lookups by agent [AI] (#81386)
* fix: scope gateway session lookups by agent

* addressing review-skill

* addressing review-skill

* addressing review-skill

* addressing review-skill

* addressing codex review

* addressing codex review

* addressing codex review

* addressing codex review

* addressing codex review

* addressing review-skill

* addressing review-skill

* addressing review-skill

* addressing review-skill

* addressing codex review

* addressing codex review

* addressing codex review

* addressing codex review

* addressing codex review

* addressing codex review

* addressing codex review

* addressing ci

* addressing ci

* fix: complete root-cause handling

* addressing review-skill

* addressing codex review

* addressing codex review

* addressing codex review

* addressing codex review

* addressing codex review

* Fix Swift protocol optional initializer defaults

* Stabilize node command lookup in approval test

* Fix browser proxy approval test node lookup

* Trim unrelated changes from issue 642 fix

* Remove unrelated formatting churn from issue 642 fix

* Fix Swift protocol generator lint

* docs: add changelog entry for PR merge
2026-05-16 22:31:02 +05:30
Vincent Koc
9a204008ba test(extensions): stabilize plugin prerelease shards 2026-05-17 01:00:43 +08:00
Peter Steinberger
bdfc078487 test(matrix): add state-after E2EE QA coverage
Adds Matrix QA coverage for the state_after E2EE regression fixed by #82631.
2026-05-16 17:59:30 +01:00
Peter Steinberger
a3e7fc7de7 test(telegram): fix cache invalidation test contexts 2026-05-16 17:56:01 +01:00
Peter Steinberger
b11f67964c test(telegram): cover bot info cache invalidation 2026-05-16 17:56:01 +01:00
Peter Steinberger
95741daeb4 fix(telegram): cache startup bot info 2026-05-16 17:56:01 +01:00
Agustin Rivera
5774517fce Fix exec allowlist wildcard target normalization (#75723)
* fix(exec): normalize allowlist wildcard targets

Co-authored-by: zsx <git@zsxsoft.com>

* fix(exec): canonicalize executable path candidates

* docs(changelog): credit exec allowlist dot-segment fix

Adds the user-facing Unreleased Fixes entry for the exec allowlist
wildcard target normalization and absolute executable path
canonicalization landed in this PR.

---------

Co-authored-by: zsx <git@zsxsoft.com>
Co-authored-by: Devin Robison <drobison@nvidia.com>
2026-05-16 09:54:26 -07:00
Shakker
405535d4ce fix: polish gateway restart diagnostics 2026-05-16 17:50:36 +01:00
samzong
92fe2a8f5f fix(gateway): improve restart readiness diagnostics 2026-05-16 17:50:36 +01:00
Peter Steinberger
597b7b0628 fix(skills): default codex review to yolo mode 2026-05-16 17:47:38 +01:00
Peter Steinberger
38cf54593e fix: accept device identity dashboard probes 2026-05-16 17:42:16 +01:00
Vincent Koc
6ed16d9356 test(extensions): harden plugin prerelease shards 2026-05-17 00:40:42 +08:00
Vincent Koc
deaf46a07d fix(auth): avoid keychain creation for oauth profile secrets 2026-05-17 00:31:30 +08:00
Vincent Koc
fddac1c507 test(extensions): align mocks with runtime contracts 2026-05-17 00:23:23 +08:00
Peter Steinberger
4526b44778 fix: preserve generated media completion attachments 2026-05-16 17:13:30 +01:00
Peter Steinberger
15b0d43412 docs: clarify plugin gateway auto-restart 2026-05-16 17:11:53 +01:00
Peter Steinberger
777d289979 fix(matrix): avoid state-after sync opt-in 2026-05-16 16:57:24 +01:00
Peter Steinberger
2fcaab0010 fix: clean up approval handler PR landing (#82482) 2026-05-16 16:41:07 +01:00
Feelw00
a2f1f73107 docs(plugins): note cancelDelivered in channel plugin interactions list
ClawSweeper R3 flagged that the previous follow-up added the
`cancelDelivered` hook to the public approval-handler runtime interaction
surface but left the channel plugin docs describing `interactions` as
only bind/unbind/clear-action hooks. Extend the bullet so plugin authors
whose `deliverPending` registers in-process or persistent state know
when to implement the cancellation hook.

AI-assisted: drafted with claude code (claude-opus-4-7).
2026-05-16 16:41:07 +01:00
Feelw00
ea9793b2e1 fix(approvals): release Matrix reaction target on mid-flight cancel
Address the ClawSweeper R2 finding that the pre-bind stopped guard
introduced in this PR drops a delivered entry without any cleanup. The
prior PR comment block was correct only for adapters whose deliverPending
has no in-process side effects; Matrix registers a reaction target in
both an in-memory Map and a persistent store inside deliverPending, so
the entry would leak until the 24h TTL (or process restart) every time
stop() landed between deliverPending and bindPending.

Add an optional cancelDelivered interaction hook on the runtime types,
forward it through both the spec-to-adapter wrapper
(createChannelApprovalNativeRuntimeAdapter) and the lazy adapter wrapper
(createLazyChannelApprovalNativeRuntimeAdapter), and invoke it from the
two stopped guards in deliverTarget: the pre-bind guard always calls it,
and the post-bind guard calls it on the branch where bindPending
returned no handle (so unbindPending cannot run). Matrix implements the
hook by calling unregisterMatrixApprovalReactionTarget on the entry's
roomId + reactionEventId, which is the exact key
registerMatrixApprovalReactionTarget uses inside deliverPending.

The other native runtime adapters (Slack, Discord, Telegram, qqbot)
leave the hook unimplemented because their deliverPending paths only
emit remote messages and keep no in-process state to drop.

Regression coverage:
- invokes cancelDelivered when stop() fires between deliverPending and
  bindPending (Deferred-gated deliverPending, asserts bindPending /
  unbindPending never run and cancelDelivered receives the entry)
- invokes cancelDelivered when stop() fires after bindPending returned
  null (asserts unbindPending stays uncalled while cancelDelivered fires)

AI-assisted: drafted with claude code (claude-opus-4-7).
2026-05-16 16:41:07 +01:00
Feelw00
851b9271a5 fix(infra): skip unbindPending without a binding handle (dts build)
The previous commit invoked unbindPending in the deliverPending→bindPending
race path before any binding existed; nativeRuntime.interactions.unbindPending
requires a binding, so the dts build failed with TS2345. In production the
race window that PROOF-CAND-040 measured is always after bindPending (3/3
trials had bindPending=1), so dropping the pre-bindPending unbindPending
call does not change observed cleanup behavior: that branch now just nulls
out the in-flight delivery. The post-bindPending branch keeps the
unbindPending call (binding handle present) and remains the only path
required to fix the leak.

The regression test is updated to park bindPending (not deliverPending)
before invoking stop(), matching the production race window.

AI-assisted: drafted with claude code (claude-opus-4-7).
2026-05-16 16:41:07 +01:00
Feelw00
06dfa6f160 fix(infra): drop in-flight approval delivery after onStopped
createChannelApprovalHandlerFromCapability shares a closure-scoped
activeEntries Map across deliverTarget / finalizeResolved /
finalizeExpired / onStopped, with no synchronization primitives in the
file. deliverTarget's two awaits (transport.deliverPending then
interactions.bindPending) bracket a read-modify-write on activeEntries;
if onStopped clears the map between those awaits, the wrapped entry is
inserted into an already-cleared map and never reaches unbindPending —
the native side keeps its listener / channel binding open forever.
Production-faithful e2e measured this 3/3 trials: bindPending=1,
unbindPending=0 per request.

Track a closure-scoped `stopped` flag set by onStopped, and have
deliverTarget call unbindPending and bail to null on each await when
stopped becomes true. nativeRuntime contracts (transport / interactions
signatures) are untouched.

AI-assisted: drafted with claude code (claude-opus-4-7).
2026-05-16 16:41:07 +01:00
Gio Della-Libera
2c59ea8a2e fix(sessions): estimate local transcript usage
Fixes #73990.\n\nAdds a transcript-derived token estimate for local/OpenAI-compatible session transcripts that have real content but no provider usage telemetry, preserving provider-reported usage when available and gating estimation on assistant model identity.\n\nVerification:\n- CI run 25965717279: success\n- Real behavior proof run 25965716561: success\n- Azure Crabbox clean-clone proof: pnpm test src/gateway/session-utils.fs.test.ts src/status/status-message.test.ts; pnpm check:changed; pnpm exec tsx /tmp/openclaw-transcript-proof.mts; git diff --check origin/main...HEAD
2026-05-16 08:40:09 -07:00
Peter Steinberger
575936473d fix(auto-reply): log suppressed message-tool-only finals (#82609)
* fix(auto-reply): fallback group finals when message tool is missed

* fix(auto-reply): log suppressed message-tool finals

* docs(auto-reply): clarify message-tool finals stay private

# Conflicts:
#	CHANGELOG.md

* docs(auto-reply): fix group visible reply examples
2026-05-16 16:30:07 +01:00
Kagura
ffdc7aa7a6 fix(slack): route DM thread replies to main session instead of thread-scoped session (#82418)
* fix(slack): route DM thread replies to main session instead of thread-scoped session

DM thread replies (user replies inside a thread under a bot message in a
DM) were routed to a thread-specific session key instead of the user's
main DM session.  This caused the agent to never receive the inbound on
the expected session, making the bot appear unresponsive.

The root cause was in prepare-routing.ts: canonicalThreadId for
isDirectMessage was set to threadTs when isThreadReply was true, creating
a session key like agent:main:slack:direct:u3🧵<ts>.  DM threads
are a UI affordance — not a session boundary — so all DM messages should
route to the main DM session regardless of thread_ts.

Also adds a diagnostic logVerbose warning when assistant_app_thread
message_changed events fail sender resolution (Case 2 of #82390),
which was previously completely silent.

Fixes #82390

* chore(slack): polish DM thread routing PR

* test(slack): update DM thread routing contract

* test(slack): flatten non-main DM thread expectations

* fix(slack): preserve bound DM thread routes

* test(slack): align DM thread session fixtures

* fix(slack): keep flattened DM thread metadata scoped

* fix(slack): preserve DM thread delivery routes

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-05-16 16:24:36 +01:00
Peter Steinberger
cc22c2ad79 docs: add codex app-server timeout changelog 2026-05-16 16:21:34 +01:00
Peter Steinberger
2074cde6cf test: stabilize codex app-server waits 2026-05-16 16:21:34 +01:00
Peter Steinberger
18cbc7bc48 test: repair current CI drift 2026-05-16 16:21:34 +01:00
Peter Steinberger
21c5f8dc6d fix(codex): keep run lane timeout progress-aware 2026-05-16 16:21:34 +01:00
Peter Steinberger
a641a27bd4 fix(codex): harden app-server progress watchdog 2026-05-16 16:21:34 +01:00
Peter Steinberger
efe3790dd3 fix(codex): preserve queued turn-start progress 2026-05-16 16:21:34 +01:00
Peter Steinberger
6778da05d6 fix(codex): scope app-server request watchdog progress 2026-05-16 16:21:34 +01:00
Eva (agent)
d7d597cfd8 fix: scope codex attempt watchdog to turn progress 2026-05-16 16:21:34 +01:00
Eva (agent)
722161271e fix: make codex app-server timeout progress-aware 2026-05-16 16:21:34 +01:00
Peter Steinberger
55439fe34b fix(agents): observe detail-less responses failures 2026-05-16 16:21:01 +01:00
Gio Della-Libera
0eca3a92e3 fix(auto-reply): preserve session model display for heartbeat usage (#82267)
* Preserve session model display for heartbeat usage
* Refresh checks after proof update
* chore: refresh CI after main repairs
2026-05-16 08:09:49 -07:00
Peter Steinberger
1769e6a2f0 fix(agents): log detail-less responses failures (#82593)
* fix(agents): log detail-less responses failures

* fix(github-copilot): guard device login fetches

* test(ci): refresh stale cron and session expectations

* test(ci): keep cron legacy string fixture

* fix(agents): redact array response failure ids

* fix(agents): classify empty response failures
2026-05-16 15:57:40 +01:00
Gio Della-Libera
0b708a2574 OC Path: restore YAML support (#81436)
* OC Path: restore YAML support
* fix(oc-path): guard yaml writes and empty sequences
* fix(oc-path): guard yaml insertion keys
* fix(oc-path): guard yaml object key
* fix(oc-path): classify yaml root insertions
* style(oc-path): format yaml branch after rebase
* fix(oc-path): reject malformed yaml edits
* docs(oc-path): clarify yaml file support
* fix(ci): refresh yaml branch after rebase
* fix(ci): clean shared blockers for yaml path PR
* fix(changelog): keep yaml path note scoped
* fix(ci): preserve current shared contracts


---------

Co-authored-by: Gio Della-Libera <giodl73@gmail.com
2026-05-16 07:52:08 -07:00
Gio Della-Libera
f7b1148bed Strip inbound metadata from replayed user turns (#82614) 2026-05-16 07:45:56 -07:00
Vincent Koc
fa9a22b960 changelog: credit TUI fallback model display fix (#82296) 2026-05-16 22:42:43 +08:00
Vincent Koc
0b24ffb91f fix(ci): keep performance artifacts on report publish failure 2026-05-16 22:41:34 +08:00
Gio Della-Libera
22858769e4 fix(tui): update model display during fallback (#82296)
* fix(tui): update fallback model display

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Refresh checks after proof update

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* chore: refresh CI after main repairs

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-16 07:35:43 -07:00
Peter Steinberger
9dedc4d95c fix: honor Codex auth order for OpenAI PI (#82605)
* fix: honor Codex auth order for OpenAI PI

* docs: add PR reference for OpenAI PI auth fix
2026-05-16 15:26:27 +01:00
Peter Steinberger
16e5d6692d fix(gateway): bound traced channel startup handoff (#82592)
* fix(gateway): bound traced channel startup handoff

* fix(github-copilot): guard device login fetches

* fix(gateway): skip stopped traced channel handoffs

* test(net): keep guarded fetch mocks hermetic
2026-05-16 15:15:57 +01:00
吴杨帆
eebdbabae9 fix: omit Ollama think for non-reasoning models
Preserve native Ollama thinking controls for supported models and explicit think=false, but avoid sending truthy think payloads for models marked reasoning=false.\n\nCo-authored-by: 吴杨帆 <85487201+leno23@users.noreply.github.com>
2026-05-16 15:10:12 +01:00
Gio Della-Libera
caf8fa2ebf Revert "Fix bundled channel dist-runtime setup roots" (#82612)
This reverts commit 1bd10cfee6.

Co-authored-by: Gio Della-Libera <giodl@microsoft.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-16 07:07:39 -07:00
Peter Steinberger
58083866d0 fix: sync codex app-server protocol drift 2026-05-16 15:03:51 +01:00
Kyzcreig
8092761d7b fix: mark codex compaction events completed 2026-05-16 15:03:51 +01:00
njuboy11
6a65ea8c3a fix: preserve post-compaction session token freshness (#82578)
Fixes #82576.

Keeps post-compaction token totals fresh across stale usage updates and adds regression coverage for the repeated auto-compaction loop. Also includes maintainer fixups needed to keep the touched CI lanes green: guarded GitHub Copilot device-flow fetches, dead-session metadata recreation, and current cron stale-data expectations.

Co-authored-by: njuboy11 <njuboy11@users.noreply.github.com>
2026-05-16 14:47:57 +01:00
Ayaan Zaidi
b9921e21b9 docs(changelog): note telegram final reply fix 2026-05-16 18:50:10 +05:30
Ayaan Zaidi
0fb0b5197e test(telegram): cover truncated progress finals 2026-05-16 18:50:10 +05:30
Ayaan Zaidi
20c3580394 fix(telegram): deliver transcript-backed final replies 2026-05-16 18:50:10 +05:30
Gio Della-Libera
1bd10cfee6 Fix bundled channel dist-runtime setup roots
* Fix bundled channel dist-runtime setup roots

Resolve bundled channel generated entries from dist-runtime before falling back to source paths, and select the dist-runtime plugin root as the boundary root for packaged setup modules. This keeps the fs-safe module open boundary check intact while preventing packaged bundled setup entries from being checked against the source extensions root.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Repair session store validation test fixtures

Update current-main tests that wrote persisted session entries without valid session IDs after session store loading started filtering invalid entries. Keep the fixture-only repair separate from the bundled channel loader fix.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Repair pairing and cron validation fixtures

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-16 06:05:34 -07:00
Vincent Koc
394f61b8ce changelog: broaden auth-profile lock bullet to cover all providers 2026-05-16 21:02:58 +08:00
Vincent Koc
a93283337e test(auth): mock locked auth profile upserts 2026-05-16 20:59:57 +08:00
Peter Steinberger
e71d10fd4d fix(webchat): show manual compaction progress
Add first-class session.operation start/end events for manual compaction and render the existing WebChat compaction indicator from those events.

Co-authored-by: Conan Scott <271909525+Conan-Scott@users.noreply.github.com>
2026-05-16 13:58:44 +01:00
Vincent Koc
f410a95081 fix(providers): lock auth setup profile writes 2026-05-16 20:52:12 +08:00
Vincent Koc
bede89dba6 fix(auth): lock cli provider auth writes 2026-05-16 20:52:12 +08:00
Vincent Koc
b0daf992b2 fix(auth): preserve locked profile upsert semantics 2026-05-16 20:52:12 +08:00
Peter Steinberger
605a2c87ae fix: carry gateway restart trace across respawn (#82396) (thanks @samzong) 2026-05-16 13:42:50 +01:00
Peter Steinberger
661362c89c docs: document gateway restart trace (#82396) (thanks @samzong) 2026-05-16 13:42:50 +01:00
samzong
587b06768f feat(gateway): add restart trace instrumentation
Signed-off-by: samzong <samzong.lu@gmail.com>
2026-05-16 13:42:50 +01:00
Peter Steinberger
862be9fb3d fix: normalize Xiaomi array tool schemas (#82575) 2026-05-16 13:34:52 +01:00
Peter Steinberger
68a4c77f5b docs: update changelog for Slack mention hints (#82152) 2026-05-16 13:33:58 +01:00
Neerav Makwana
6b8f3fd206 fix(slack): clarify mention prompt guidance 2026-05-16 13:33:58 +01:00
Peter Steinberger
1426112f95 fix: finalize memory slot warning 2026-05-16 13:26:51 +01:00
Gio Della-Libera
0204c522bb fix(config): keep blocked memory slots fatal
Preserve hard validation failures for official external memory slot plugins that are blocked by registry diagnostics, while keeping missing uninstalled official memory plugins warning-only.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-16 13:26:51 +01:00
Gio Lodi
d1787b73db fix(config): warn for missing official memory slot
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-16 13:26:51 +01:00
Peter Steinberger
d16efadc00 fix(gateway): quiet startup retry closes
Co-authored-by: JARVIS-Glasses <284122573+JARVIS-Glasses@users.noreply.github.com>
Co-authored-by: WhatsSkiLL <284126683+IWhatsskill@users.noreply.github.com>
2026-05-16 13:26:00 +01:00
Peter Steinberger
7593ba8623 test: type daemon gateway auth mock 2026-05-16 13:22:24 +01:00
Peter Steinberger
c4d65e45da docs: update changelog for #81112 2026-05-16 13:22:24 +01:00
Eva (agent)
60b4105665 fix: migrate plugin tool contracts in doctor 2026-05-16 13:22:24 +01:00
Frank Yang
e6d04550ca fix(gateway): route WebChat images through imageModel
Route WebChat image attachments through the configured imageModel when the active session model cannot accept images, while keeping one-turn image auth and fallback state ephemeral.

Thanks @frankekn.
2026-05-16 20:12:02 +08:00
Peter Steinberger
e0870473b2 docs: update changelog for Android links (#82392) 2026-05-16 13:06:26 +01:00
Neerav Makwana
07d2043081 fix(android): make chat links tappable 2026-05-16 13:06:26 +01:00
Peter Steinberger
03a7b19228 fix: recover gateway dashboard startup in stripped shells 2026-05-16 13:06:19 +01:00
Vincent Koc
7c70954892 docs(agents): clarify crabbox testbox routing 2026-05-16 20:05:50 +08:00
Vincent Koc
192caba631 fix(export): report malformed transcript rows (#82553) 2026-05-16 20:03:28 +08:00
Vincent Koc
d32b2a4771 changelog: note Codex attribution scoped to local transcripts 2026-05-16 20:02:28 +08:00
Alex Knight
c438dadc5c Fix Claude CLI runtime migration for gateway turns (#82546)
Summary:
- The PR adds model-scoped `claude-cli` runtime policy to Anthropic CLI migration/default backfill, updates the gateway CLI live-smoke config, tests, and changelog.
- Reproducibility: yes. source inspection gives a high-confidence reproduction path: current main writes `clau ... del/provider-scoped runtime policy. I did not run a live Telegram/Dashboard repro in this read-only review.

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

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

Prepared head SHA: 62cf54484f
Review: https://github.com/openclaw/openclaw/pull/82546#issuecomment-4466676206

Co-authored-by: Alex Knight <15041791+amknight@users.noreply.github.com>
2026-05-16 11:54:48 +00:00
Alex Knight
f8b7008f7c Fix Kimi Coding tool-call replay (#82550)
Summary:
- The PR preserves Kimi Coding reasoning_content replay for OpenAI-compatible tool-call follow-up turns, extends replay model-id matching, adds Kimi wrapper/tests, and updates the changelog.
- Reproducibility: yes. at source level: current main drops or fails to synthesize reasoning_content for kimi- ... es a concrete Kimi 400 after tool-call history. I did not run a live Kimi request in this read-only review.

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

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

Prepared head SHA: 9a4605ee38
Review: https://github.com/openclaw/openclaw/pull/82550#issuecomment-4466701075

Co-authored-by: Alex Knight <15041791+amknight@users.noreply.github.com>
2026-05-16 11:54:46 +00:00
Peter Steinberger
9558b2c222 fix(channels): install externalized same-id adds 2026-05-16 12:53:31 +01:00
Mason Huang
5a14b1c5c5 fix(maintainer): gate body notifications after redaction (#81993)
* feat(secret-scanning): advise delete-and-recreate for issue/PR body leaks

* fix(maintainer): gate body notifications
2026-05-16 19:50:26 +08:00
Peter Steinberger
9b560b8a41 fix: limit Codex attribution to local transcripts
Summary:
- Limit canonical OpenAI Codex app-server attribution rewrites to local transcript and trajectory records.
- Keep runtime/tool routing on the selected OpenAI model metadata, including OpenAI API-key backup profiles.
- Fix the current gateway-readiness lint blocker that was red on main.

Verification:
- codex-review branch helper clean with focused Codex app-server tests.
- pnpm lint --threads=8
- pnpm test src/commands/gateway-readiness.test.ts
- GitHub CI run 25960997256 green.

Co-authored-by: Eva (agent) <eva+agent-78055@100yen.org>
2026-05-16 12:45:11 +01:00
Peter Steinberger
c6af9908e7 fix: hide decorative emoji on unsupported terminals (#82556)
* fix: hide decorative emoji on unsupported terminals

* chore: fix gateway readiness lint
2026-05-16 12:39:13 +01:00
Peter Steinberger
67fb1df352 docs: prefer clean refactors over compat shims 2026-05-16 12:38:51 +01:00
Peter Steinberger
87592fdfe2 ci: fix gateway readiness lint 2026-05-16 12:38:26 +01:00
Eva (agent)
beb3311a62 test: cover retained repair backup cleanup failures 2026-05-16 12:38:26 +01:00
Eva (agent)
e1d7ba5915 fix(agents): remove transient session-repair backups
Adapts @tynamite's fix from the abandoned #77945 to current main (which
moved to replaceFileAtomic after that PR was opened), and adds the docs +
changelog updates clawsweeper flagged plus a regression test for the
field condition from #80960.

When repairSessionFileIfNeeded writes a cleaned transcript, the sibling
*.bak-<pid>-<ts> snapshot is deleted after the atomic replace succeeds.
It is only retained — and only then reported via backupPath — when the
cleanup itself fails. This prevents the unbounded accumulation observed
in #80960, where a stuck operations-agent session with a persistently
malformed JSONL line caused 2,180 ~1.8 MB backup files to pile up over
~25 hours inside two gateway processes (PIDs 1220 and 2640).

Test changes:
- Replace requireBackupPath helper with expectNoRetainedBackup that
  also asserts no .bak-* siblings remain on disk.
- Update the four call sites that used to read the retained backup.
- Add a regression test that drives repair five times against a file
  with a recurring malformed tail and asserts zero retained backups.

Docs:
- docs/reference/transcript-hygiene.md: describe backup as transient,
  retained only on cleanup failure.

Fixes #80960. Supersedes #77945. Co-authored by @tynamite — credit for
the original approach.

Co-authored-by: tynamite <35367599+tynamite@users.noreply.github.com>
2026-05-16 12:38:26 +01:00
Peter Steinberger
bf0141a753 docs: use take-control for webvnc handoffs 2026-05-16 12:37:43 +01:00
Peter Steinberger
210ff7d318 fix(agents): yield during model stream bursts 2026-05-16 12:18:36 +01:00
WhatsSkiLL
f50c65f124 fix(codex): release raw assistant app-server completions [AI-assisted] (#82403)
* fix(codex): release raw assistant app-server completions

* refactor(codex): simplify raw assistant release guard

* fix(codex): ignore commentary raw assistant completions

* docs: add codex app-server completion changelog

---------

Co-authored-by: JARVIS-Glasses <284122573+JARVIS-Glasses@users.noreply.github.com>
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-05-16 12:13:54 +01:00
Peter Steinberger
b5ba210fd5 fix: gate dashboard on gateway readiness 2026-05-16 12:11:56 +01:00
Peter Steinberger
9dea44ef6d fix: simplify empty status sections 2026-05-16 12:03:33 +01:00
Vincent Koc
3e339cde89 fix(tts): honor preferred provider aliases 2026-05-16 18:59:06 +08:00
Vincent Koc
01eb56e45a fix(backup): hide manifest parser internals (#82539) 2026-05-16 18:56:45 +08:00
Peter Steinberger
225e48f632 fix(agents): fall back to PI when Codex harness is unavailable (#82532)
* fix(agents): fall back to pi when codex harness is unavailable

* fix(agents): align codex fallback auth routing
2026-05-16 11:42:06 +01:00
Vincent Koc
97e86fb2da Merge branch 'main' of https://github.com/openclaw/openclaw
* 'main' of https://github.com/openclaw/openclaw:
  fix(providers): honor cleared video provider options
2026-05-16 18:38:18 +08:00
Vincent Koc
a85cd65775 fix(plugins): deprecate deactivate hook alias 2026-05-16 18:36:27 +08:00
Vincent Koc
625ff8531f fix(providers): honor cleared video provider options 2026-05-16 18:34:30 +08:00
Vincent Koc
7e4929e004 fix(sessions): validate replayed transcript rows 2026-05-16 18:29:30 +08:00
Vincent Koc
310c8530eb fix(agents): preserve trusted tool media provenance 2026-05-16 18:28:59 +08:00
Vincent Koc
856076079e fix(gateway): require trusted local audio display media 2026-05-16 18:20:24 +08:00
Vincent Koc
863069e2c6 fix(trajectory): stop cyclic transcript exports 2026-05-16 18:09:40 +08:00
Vincent Koc
46a67d30af fix(ci): restore persisted state guards 2026-05-16 18:06:13 +08:00
Peter Steinberger
b05fcad7a7 docs: add Crabbox human handoff preflight 2026-05-16 11:02:08 +01:00
Vincent Koc
9eeb17fa82 fix(providers): harden search tool response schemas 2026-05-16 18:00:31 +08:00
Vincent Koc
33be0fbea7 fix(plugins): accept deactivate hook alias 2026-05-16 17:47:14 +08:00
Peter Steinberger
6171b4254d fix(model-picker): show effective runtime choices 2026-05-16 10:34:49 +01:00
Peter Steinberger
7e0e29ef17 fix(cron): preserve current session identity after compaction 2026-05-16 10:34:48 +01:00
Sahil Satralkar
6d25ae5e0c fix(cli): finalize context engine turns (#81869)
* fix(cli): finalize context engine turns

* fix(cli): avoid context engine prepare leak

* fix(cli): keep context engine alive after turns

* fix(cli): complete context engine lifecycle

* fix(cli): preserve context engine maintenance contracts

* fix(cli): align context engine transcript finalization

* fix(cli): close context engine lifecycle gaps

* fix(cli): keep context engine snapshots current

* fix(cli): preserve context snapshot entry types

* fix(cli): clean up failed context engine prepare

* fix(cli): detect resolved session transcripts

* fix(cli): preserve context engine lifecycle ownership

* docs(changelog): credit CLI context engine fix

---------

Co-authored-by: Frank Yang <frank.ekn@gmail.com>
2026-05-16 17:28:36 +08:00
Peter Steinberger
9c5acb7ea3 chore: release 2026.5.17 2026-05-16 10:11:41 +01:00
Ted Li
832b65ccea fix(agents): preserve Pi tool result error flags (#81546) (#81564)
Merged via squash.

Prepared head SHA: 320c867fda
Co-authored-by: MonkeyLeeT <6754057+MonkeyLeeT@users.noreply.github.com>
Co-authored-by: hxy91819 <8814856+hxy91819@users.noreply.github.com>
Reviewed-by: @hxy91819
2026-05-16 17:09:22 +08:00
Frank Yang
adca6e9c55 fix(gateway): abort gmail watcher reload starts (#82499)
* fix(gateway): abort gmail watcher reload starts

* fix(gateway): prevent stopped gmail reload starts
2026-05-16 16:41:43 +08:00
Ayaan Zaidi
aedcf0f897 fix(telegram): recover polling spool after restart (#82256) (thanks @VACInc) 2026-05-16 14:04:47 +05:30
Ayaan Zaidi
89a3b9a07e fix(telegram): avoid stealing live spool claims 2026-05-16 14:04:47 +05:30
Ayaan Zaidi
784ee94108 test(telegram): align spooled claim expectations 2026-05-16 14:04:47 +05:30
Ayaan Zaidi
494517a990 refactor(telegram): simplify spool claim recovery 2026-05-16 14:04:47 +05:30
VACInc
59d2f88e41 fix(telegram): recover restart spool claims 2026-05-16 14:04:47 +05:30
Josh Avant
c5b3352326 Fix doctor repair for disabled Codex runtime plugin (#82502)
* fix doctor codex plugin runtime repair

* add changelog for codex doctor repair

* avoid implicit codex repair without agent routes

* expect codex repair in doctor config flow
2026-05-16 03:16:54 -05:00
Josh Avant
e57b137aef fix(codex): enforce native tool policy (#82496)
* fix(codex): enforce native tool policy

* docs: add changelog for codex native policy fix

* fix(codex): satisfy native hook relay lint
2026-05-16 03:02:28 -05:00
Josh Avant
23f73b3ecf Fix session reset files and ACPX orphan reaping (#82459)
* fix gateway reset transcript rotation

* fix acpx orphan adapter reaping

* add changelog for pr 82459

* fix ci session metadata normalization

* preserve canonical session ids in store reads

* fix session metadata id normalization
2026-05-16 02:43:30 -05:00
samzong
2fa853dce5 fix(gateway): isolate gmail watcher restart and abort handling (#82395)
Merged via squash.

Prepared head SHA: 61502846df
Co-authored-by: samzong <13782141+samzong@users.noreply.github.com>
Co-authored-by: frankekn <4488090+frankekn@users.noreply.github.com>
Reviewed-by: @frankekn
2026-05-16 15:31:51 +08:00
Vincent Koc
e975c3b212 changelog: dedupe MCP cancellation bullets, add Fixes #82424 2026-05-16 15:02:41 +08:00
joshavant
df0d061c7a docs: add mcp tool cancellation changelog 2026-05-16 01:53:47 -05:00
Josh Avant
916234977a fix(telegram): drop expired approval callbacks (#82455)
* fix(telegram): drop expired approval callbacks

* changelog: credit expired Telegram callback fix
2026-05-16 01:43:17 -05:00
Vincent Koc
e650f8930d fix(test): avoid scanning plugin archive entries 2026-05-16 14:35:24 +08:00
Vincent Koc
31bbe6eb0f fix(test): avoid scanning status runtime bundles 2026-05-16 14:31:43 +08:00
Vincent Koc
f1f7525aa5 fix(test): avoid scanning legacy config ownership 2026-05-16 14:29:06 +08:00
Vincent Koc
2be9abfb1c fix(test): avoid scanning bundled plugin metadata 2026-05-16 14:26:17 +08:00
Vincent Koc
4b940f66fd fix(test): avoid scanning live shard roots 2026-05-16 14:22:48 +08:00
Vincent Koc
41b4eb97f0 fix(test): avoid scanning prompt snapshots 2026-05-16 14:20:17 +08:00
Vincent Koc
8b7d28354e fix(test): avoid scanning plugin docs examples 2026-05-16 14:16:28 +08:00
Vincent Koc
2bcb0abbb8 fix(test): avoid scanning channel docs examples 2026-05-16 14:13:40 +08:00
Vincent Koc
f9d015346e fix(test): avoid scanning fs-safe import boundary 2026-05-16 14:10:45 +08:00
Vincent Koc
2bdbf240a9 fix(media): avoid staged image extensions for containers 2026-05-16 14:09:39 +08:00
Vincent Koc
6920ec6c54 fix(config): preserve include writes with plugin validation skip 2026-05-16 14:08:36 +08:00
Vincent Koc
5d4166a368 fix(test): avoid scanning publishable plugin packages 2026-05-16 14:07:47 +08:00
Vincent Koc
306ca4d3eb fix(media): distrust image hints for container bytes 2026-05-16 14:05:02 +08:00
Vincent Koc
3bc7d4061b fix(gateway): preserve partial transcript branches 2026-05-16 14:04:41 +08:00
Vincent Koc
ae42768e5f fix(test): avoid scanning bundled plugin naming guardrails 2026-05-16 14:04:14 +08:00
Vincent Koc
c98ccbe513 fix(test): avoid scanning runtime registry boundary 2026-05-16 14:01:15 +08:00
Vincent Koc
bd81a0323c fix(pairing): skip malformed pending pairing requests 2026-05-16 13:59:23 +08:00
Vincent Koc
bf7bd8dcd1 fix(test): avoid scanning outbound threading guardrails 2026-05-16 13:56:59 +08:00
Josh Avant
4159a11ea3 Fix stale bootstrap file breaking channel agent turns (#82463)
* fix agents stale bootstrap context

* docs changelog stale bootstrap fix
2026-05-16 00:55:18 -05:00
Vincent Koc
da8c11aaae fix(test): avoid scanning tool boundary modules 2026-05-16 13:53:17 +08:00
Gio Della-Libera
dccf5f6842 fix(gateway): fire session:patch hooks for model changes (#82257)
* Fire session patch hooks for model changes

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* chore: refresh CI after main repairs

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-15 22:44:01 -07:00
Gio Della-Libera
8c9ec0724e fix(agents): honor disabled reasoning in thinking policy (#81454)
* fix(agents): honor disabled reasoning in thinking policy

* test: refresh thinking policy CI fixtures

* test: align thinking policy CI guardrails

---------

Co-authored-by: Gio Della-Libera <giodl@microsoft.com>
2026-05-15 22:33:43 -07:00
Gio Della-Libera
9aec9200f1 fix(agents): honor OPENCLAW_WORKSPACE_DIR fallback (#81447)
Co-authored-by: Gio Della-Libera <giodl@microsoft.com>
2026-05-15 22:32:02 -07:00
Gio Della-Libera
d7d85d1eb6 fix(cron): bootstrap external channel delivery targets (#82371)
* fix(cron): bootstrap external channel delivery targets

Allow isolated cron delivery target resolution to opt into outbound channel plugin bootstrap when the loaded-plugin fast path misses. Thread the explicit allowBootstrap flag through resolveOutboundTarget so externalized non-startup channel plugins can be resolved without changing default hot-path behavior.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* chore: refresh proof for cron bootstrap PR

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-15 22:31:56 -07:00
Vincent Koc
0642b0033b fix(plugins): reject omitted package runtime files 2026-05-16 13:18:54 +08:00
Vincent Koc
7f46876a5d fix(auth): sanitize persisted device auth tokens 2026-05-16 13:18:22 +08:00
Vincent Koc
29951fdbb7 fix(test): avoid scanning bundled channel shape roots 2026-05-16 13:17:53 +08:00
Josh Avant
51f4a5e8a0 Fix Telegram presentation-only payload sends (#82449)
* fix telegram presentation payload fallback

* changelog telegram presentation payload fallback

* fix telegram presentation reply delivery
2026-05-16 00:16:51 -05:00
Vincent Koc
92492ecdd0 fix(gateway): skip malformed compaction checkpoints 2026-05-16 13:14:56 +08:00
Vincent Koc
ba48d162af fix(commitments): sanitize persisted reminder metadata 2026-05-16 13:12:01 +08:00
Vincent Koc
af8d2f2948 fix(config): harden persisted boundary repair 2026-05-16 13:12:00 +08:00
Vincent Koc
37ba583b05 fix(sessions): validate persisted entry metadata 2026-05-16 13:12:00 +08:00
Vincent Koc
05b3774c28 fix(cron): skip malformed persisted jobs 2026-05-16 13:11:59 +08:00
Vincent Koc
24e4dc68b7 fix(tasks): validate persisted requester origins 2026-05-16 13:11:59 +08:00
Vincent Koc
2c9f284c1e fix(test): avoid walking plugin boundary invariant scans 2026-05-16 13:09:36 +08:00
Vincent Koc
80a563b4e4 changelog: credit MCP plugin tool abort-signal forwarding (#82443) 2026-05-16 13:03:15 +08:00
Josh Avant
b7d61c8daf fix: forward MCP tool abort signals (#82443)
* fix: forward MCP tool abort signals

* test: repair doctor health context fixtures
2026-05-16 00:01:10 -05:00
Gio Della-Libera
c8bec51869 OC Path: add dry-run diff output (#81437)
* OC Path: add dry-run diff output

* fix(oc-path): require dry-run for diff output

* fix(oc-path): show final newline diff

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(oc-path): show line-ending-only dry-run diffs

---------

Co-authored-by: Gio Della-Libera <giodl@microsoft.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-15 21:54:06 -07:00
hcl
f436b4310a fix(feishu): stream CardKit text deltas (#82419)
Fixes #82417.

Co-authored-by: hclsys <7755017+hclsys@users.noreply.github.com>
2026-05-16 12:51:55 +08:00
Vincent Koc
f78434985a fix(update): skip plugin validation during package repair reads 2026-05-16 12:44:33 +08:00
Vincent Koc
33685e1474 fix(codex): remove redundant context text coercion 2026-05-16 12:30:59 +08:00
Vincent Koc
abf78a0727 fix(providers): reject malformed poll status payloads 2026-05-16 12:20:42 +08:00
Vincent Koc
bc81d243ba fix(providers): harden model catalog response schemas 2026-05-16 12:16:42 +08:00
Vincent Koc
c8c6df73a9 fix(providers): harden embedding response schemas 2026-05-16 12:16:42 +08:00
Vincent Koc
202dd7590d fix(providers): harden audio response schemas 2026-05-16 12:16:41 +08:00
Josh Lehman
639107b7db fix: repair codex context-engine test typing 2026-05-15 21:14:54 -07:00
Vincent Koc
872e068470 fix(test): avoid walking plugin sdk subpath scans 2026-05-16 12:14:10 +08:00
Vincent Koc
0eef4c49f6 fix(plugins): ignore malformed catalog metadata 2026-05-16 12:09:22 +08:00
Josh Avant
0240cc578c fix: repair source-only official plugin installs (#82425)
* fix: repair source-only official plugin installs

* docs: add changelog for official plugin repair
2026-05-15 23:04:15 -05:00
Josh Lehman
80ca48418a feat(codex): bind context-engine projections to codex threads (#82351)
* feat(codex): bind context-engine projections to codex threads

* fix: harden Codex context-engine projection

* fix: remove unused Codex projection helper

* fix(codex): adopt compacted context-engine transcripts
2026-05-15 20:59:38 -07:00
2694 changed files with 152611 additions and 27194 deletions

View File

@@ -0,0 +1,130 @@
---
name: autoreview
description: "Autoreview closeout: local dirty changes, PR branch vs main, parallel tests."
---
# Autoreview
Run Codex's built-in code review as a closeout check. This is code review (`codex 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.
Use when:
- user asks for Codex review / autoreview / second-model review
- after non-trivial code edits, before final/commit/ship
- reviewing a local branch or PR branch after fixes
## Contract
- Treat review output as advisory. Never blindly apply it.
- Verify every finding by reading the real code path and adjacent files.
- 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. If Codex is unavailable or exits with an error, the helper falls back to the first configured CLI from `claude -p`, `pi -p`, `opencode run`, `droid exec`, or `copilot`. Prefer Codex for final closeout because it uses native review mode; non-Codex reviewers use a Codex-inspired generated diff prompt. 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.
- 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.
- 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.
- Do not push just to review. Push only when the user requested push/ship/PR update.
## Pick Target
Dirty local work:
```bash
codex review --uncommitted
```
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
or branch diff instead; do not force `--mode local` / `--uncommitted` just
because the helper docs mention dirty work first. A clean `--uncommitted` review
only proves there is no local patch.
Branch/PR work:
```bash
git fetch origin
codex review --base origin/main
```
Do not pass an inline prompt with `--base`; current CLI rejects `--base` + `[PROMPT]` even though help text is ambiguous. If custom instructions are needed, run the plain base review first, then do a local/manual follow-up pass.
If an open PR exists, use its actual base:
```bash
base=$(gh pr view --json baseRefName --jq .baseRefName)
codex review --base "origin/$base"
```
Committed single change:
```bash
codex review --commit HEAD
```
or with the helper:
```bash
.agents/skills/autoreview/scripts/autoreview --mode commit --commit HEAD
```
Use commit review for already-landed or already-pushed work on `main`. Reviewing
clean `main` against `origin/main` is usually an empty diff after push. For a
small stack, review each commit explicitly or review the branch before merging
with `--base`.
## Parallel Closeout
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>"
```
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.
## 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.
## Helper
Bundled helper:
```bash
.agents/skills/autoreview/scripts/autoreview --help
```
The helper:
- chooses dirty `--uncommitted` first
- otherwise uses current PR base if `gh pr view` works
- otherwise uses `origin/main` for non-main branches
- 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 configured CLI fallback
- 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
- 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
- prints `autoreview clean: no accepted/actionable findings reported` when the selected review command exits 0
## 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.

View File

@@ -0,0 +1,630 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'EOF'
Usage: autoreview [options]
Options:
--mode auto|local|branch|commit
Target selection. Default: auto.
--base REF Base ref for branch review. Default: PR base or origin/main.
--commit REF Commit ref for commit review. Default: HEAD.
--reviewer codex|claude|pi|opencode|droid|copilot|auto
Review engine. Default: Codex with configured fallback on error.
--fallback-reviewer auto|claude|pi|opencode|droid|copilot|none
Fallback when Codex is unavailable or exits nonzero. Default: auto.
--codex-bin PATH Codex binary. Default: codex.
--claude-bin PATH Claude binary. Default: claude.
--pi-bin PATH Pi binary. Default: pi.
--opencode-bin PATH OpenCode binary. Default: opencode.
--droid-bin PATH Droid binary. Default: droid.
--copilot-bin PATH GitHub Copilot binary. Default: copilot.
--full-access Keep yolo/full-access mode enabled. Default.
--no-yolo Run nested Codex review with normal sandbox/approval prompts.
--output FILE Also save output to file.
--parallel-tests CMD Run review and test command concurrently.
--dry-run Print selected commands, do not run.
-h, --help Show help.
Modes:
local codex review --uncommitted
branch codex review --base <base>
commit codex review --commit <commit>
auto dirty tree -> local, else PR/current branch -> branch
EOF
}
mode=auto
base_ref=
commit_ref=HEAD
reviewer=${AUTOREVIEW_REVIEWER:-${CODEX_REVIEW_REVIEWER:-auto}}
fallback_reviewer=${AUTOREVIEW_FALLBACK_REVIEWER:-${CODEX_REVIEW_FALLBACK_REVIEWER:-auto}}
codex_bin=${CODEX_BIN:-codex}
claude_bin=${CLAUDE_BIN:-claude}
pi_bin=${PI_BIN:-pi}
opencode_bin=${OPENCODE_BIN:-opencode}
droid_bin=${DROID_BIN:-droid}
copilot_bin=${COPILOT_BIN:-copilot}
codex_args=()
yolo=${AUTOREVIEW_YOLO:-${CODEX_REVIEW_YOLO:-1}}
output=${AUTOREVIEW_OUTPUT:-${CODEX_REVIEW_OUTPUT:-}}
parallel_tests=
dry_run=false
while [[ $# -gt 0 ]]; do
case "$1" in
--mode)
mode=${2:-}
shift 2
;;
--base)
base_ref=${2:-}
shift 2
;;
--commit)
commit_ref=${2:-}
shift 2
;;
--reviewer)
reviewer=${2:-}
shift 2
;;
--fallback-reviewer)
fallback_reviewer=${2:-}
shift 2
;;
--codex-bin)
codex_bin=${2:-}
shift 2
;;
--claude-bin)
claude_bin=${2:-}
shift 2
;;
--pi-bin)
pi_bin=${2:-}
shift 2
;;
--opencode-bin)
opencode_bin=${2:-}
shift 2
;;
--droid-bin)
droid_bin=${2:-}
shift 2
;;
--copilot-bin)
copilot_bin=${2:-}
shift 2
;;
--full-access)
yolo=1
shift
;;
--no-yolo)
yolo=0
shift
;;
--output)
output=${2:-}
shift 2
;;
--parallel-tests)
parallel_tests=${2:-}
shift 2
;;
--dry-run)
dry_run=true
shift
;;
-h|--help)
usage
exit 0
;;
*)
usage >&2
exit 2
;;
esac
done
case "$yolo" in
0|false|False|FALSE|no|No|NO|off|Off|OFF) ;;
*) codex_args+=(--dangerously-bypass-approvals-and-sandbox --sandbox danger-full-access) ;;
esac
case "$mode" in
auto|local|branch|commit) ;;
*)
echo "invalid --mode: $mode" >&2
exit 2
;;
esac
case "$reviewer" in
auto|codex|claude|pi|opencode|droid|copilot) ;;
*)
echo "invalid --reviewer: $reviewer" >&2
exit 2
;;
esac
case "$fallback_reviewer" in
auto|claude|pi|opencode|droid|copilot|none) ;;
*)
echo "invalid --fallback-reviewer: $fallback_reviewer" >&2
exit 2
;;
esac
repo_root=$(git rev-parse --show-toplevel)
current_branch=$(git branch --show-current 2>/dev/null || true)
dirty=false
if [[ -n "$(git status --porcelain)" ]]; then
dirty=true
fi
pr_url=
if [[ -z "$base_ref" && "$mode" != local ]] && command -v gh >/dev/null 2>&1; then
if pr_lines=$(gh pr view --json baseRefName,url --jq '[.baseRefName, .url] | @tsv' 2>/dev/null); then
base_name=${pr_lines%%$'\t'*}
pr_url=${pr_lines#*$'\t'}
if [[ -n "$base_name" ]]; then
base_ref="origin/$base_name"
fi
fi
fi
if [[ -z "$base_ref" ]]; then
base_ref=origin/main
fi
review_kind=
if [[ "$mode" == local || ( "$mode" == auto && "$dirty" == true ) ]]; then
review_kind=local
elif [[ "$mode" == commit ]]; then
review_kind=commit
elif [[ "$mode" == branch || ( "$mode" == auto && -n "$current_branch" && "$current_branch" != "main" ) ]]; then
review_kind=branch
else
echo "no review target: clean main checkout and no forced mode" >&2
exit 1
fi
if [[ "$review_kind" == local ]]; then
review_cmd=("$codex_bin" "${codex_args[@]}" review --uncommitted)
elif [[ "$review_kind" == commit ]]; then
review_cmd=("$codex_bin" "${codex_args[@]}" review --commit "$commit_ref")
else
review_cmd=("$codex_bin" "${codex_args[@]}" review --base "$base_ref")
fi
printf 'autoreview target: %s\n' "$review_kind"
printf 'branch: %s\n' "${current_branch:-detached}"
if [[ -n "$pr_url" ]]; then
printf 'pr: %s\n' "$pr_url"
fi
if [[ "$reviewer" == auto ]]; then
printf 'reviewer: codex\n'
else
printf 'reviewer: %s\n' "$reviewer"
fi
case "$reviewer" in
codex|auto) ;;
*)
printf 'note: Codex native review mode is the recommended and best-supported review path; %s uses a generated diff prompt.\n' "$reviewer"
;;
esac
if [[ "$reviewer" == auto || "$reviewer" == codex ]]; then
printf 'review:'
printf ' %q' "${review_cmd[@]}"
printf '\n'
else
printf 'review: %s prompt review\n' "$reviewer"
fi
if [[ -n "$parallel_tests" ]]; then
printf 'tests: %s\n' "$parallel_tests"
fi
if [[ "$review_kind" == branch ]]; then
printf 'fetch: git fetch origin --quiet\n'
fi
if [[ -n "$output" ]]; then
printf 'output: %s\n' "$output"
fi
if [[ "$dry_run" == true ]]; then
exit 0
fi
if [[ "$review_kind" == branch ]]; then
git fetch origin --quiet || {
echo "warning: git fetch origin failed; reviewing with existing refs" >&2
}
fi
review_output=$output
review_output_is_temp=false
if [[ -z "$review_output" ]]; then
review_output=$(mktemp)
review_output_is_temp=true
fi
mkdir -p "$(dirname "$review_output")"
: > "$review_output"
cleanup() {
if [[ "${review_output_is_temp:-false}" == true && -n "${review_output:-}" ]]; then
rm -f "$review_output"
fi
if [[ -n "${prompt_file:-}" ]]; then
rm -f "$prompt_file"
fi
}
trap cleanup EXIT
run_review() {
mkdir -p "$(dirname "$review_output")"
"${review_cmd[@]}" 2>&1 | tee "$review_output"
}
diff_for_review() {
case "$review_kind" in
local)
git -C "$repo_root" diff --stat
git -C "$repo_root" diff --cached --stat
git -C "$repo_root" diff --find-renames
git -C "$repo_root" diff --cached --find-renames
while IFS= read -r untracked_file; do
[[ -n "$untracked_file" ]] || continue
git -C "$repo_root" diff --no-index -- /dev/null "$untracked_file" || true
done < <(git -C "$repo_root" ls-files --others --exclude-standard)
;;
commit)
git -C "$repo_root" show --find-renames --stat --format=fuller "$commit_ref"
git -C "$repo_root" show --find-renames --format=medium "$commit_ref"
;;
branch)
git -C "$repo_root" diff --find-renames --stat "$base_ref"...HEAD
git -C "$repo_root" diff --find-renames "$base_ref"...HEAD
;;
esac
}
build_prompt_file() {
prompt_file=$(mktemp)
{
cat <<EOF
You are performing a closeout code review for the current repository.
Review target: $review_kind
Branch: ${current_branch:-detached}
Base: ${base_ref:-}
Commit: ${commit_ref:-}
Rules:
- Review the proposed code change as a closeout reviewer.
- Focus on the diff below. If your CLI exposes read-only repository tools, inspect surrounding code and tests to verify findings; never modify files.
- Do not modify files.
- Report only discrete, actionable issues introduced by this change.
- Prioritize correctness, regressions, security, data loss, performance cliffs, and missing tests that would catch a real bug.
- Do not report pre-existing issues, speculative risks, broad rewrites, style nits, changelog gaps, or findings that depend on unstated assumptions.
- Identify the concrete scenario where the issue appears, and keep the line reference as small as possible.
- A finding should overlap changed code or clearly cite changed code as the cause.
- For each accepted/actionable finding, use exactly this format:
[P<0-3>] Short title
File: path:line
Why: one sentence
Fix: one sentence
- If no accepted/actionable findings, output exactly:
autoreview clean: no accepted/actionable findings reported
Diff:
EOF
diff_for_review
} > "$prompt_file" || return
}
reviewer_output_has_clean_marker() {
local path=$1
grep -Eq '^[^[:alnum:]]*autoreview clean: no accepted/actionable findings reported[[:space:]]*$' "$path"
}
run_prompt_reviewer() {
local selected=$1
local copilot_prompt=
local prompt_bytes=0
local reviewer_output
local status=0
if ! build_prompt_file; then
rm -f "${prompt_file:-}"
prompt_file=
return 1
fi
reviewer_output=$(mktemp)
mkdir -p "$(dirname "$review_output")"
case "$selected" in
claude)
if ! command -v "$claude_bin" >/dev/null 2>&1; then
echo "fallback reviewer unavailable: $claude_bin" >&2
status=127
elif printf 'fallback: claude -p\n' | tee -a "$review_output"; then
"$claude_bin" --tools "" --no-session-persistence -p < "$prompt_file" 2>&1 | tee -a "$review_output" "$reviewer_output"
status=$?
else
status=$?
fi
;;
pi)
if ! command -v "$pi_bin" >/dev/null 2>&1; then
echo "fallback reviewer unavailable: $pi_bin" >&2
status=127
elif printf 'fallback: pi -p\n' | tee -a "$review_output"; then
"$pi_bin" --no-tools --no-session -p < "$prompt_file" 2>&1 | tee -a "$review_output" "$reviewer_output"
status=$?
else
status=$?
fi
;;
opencode)
if ! command -v "$opencode_bin" >/dev/null 2>&1; then
echo "fallback reviewer unavailable: $opencode_bin" >&2
status=127
elif printf 'fallback: opencode run\n' | tee -a "$review_output"; then
"$opencode_bin" run --pure --dir "$repo_root" \
"Review the attached prompt file. Do not modify files." \
--file "$prompt_file" 2>&1 | tee -a "$review_output" "$reviewer_output"
status=$?
else
status=$?
fi
;;
droid)
if ! command -v "$droid_bin" >/dev/null 2>&1; then
echo "fallback reviewer unavailable: $droid_bin" >&2
status=127
elif printf 'fallback: droid exec\n' | tee -a "$review_output"; then
"$droid_bin" exec --cwd "$repo_root" -f "$prompt_file" 2>&1 | tee -a "$review_output" "$reviewer_output"
status=$?
else
status=$?
fi
;;
copilot)
if ! command -v "$copilot_bin" >/dev/null 2>&1; then
echo "fallback reviewer unavailable: $copilot_bin" >&2
status=127
elif printf 'fallback: copilot\n' | tee -a "$review_output"; then
prompt_bytes=$(wc -c < "$prompt_file" | tr -d '[:space:]')
if (( prompt_bytes > 120000 )); then
echo "copilot reviewer unavailable: generated prompt is too large for copilot -p; use codex, droid, or another file/stdin-capable reviewer" \
2>&1 | tee -a "$review_output" "$reviewer_output"
status=1
else
copilot_prompt=$(< "$prompt_file")
"$copilot_bin" -C "$repo_root" --available-tools=none --stream off --output-format text --silent \
-p "$copilot_prompt" \
2>&1 | tee -a "$review_output" "$reviewer_output"
status=$?
fi
else
status=$?
fi
;;
*)
echo "unsupported prompt reviewer: $selected" >&2
status=2
;;
esac
if [[ "$status" == 0 ]]; then
if grep -Eq '\[P[0-3]\]' "$reviewer_output"; then
status=1
elif ! grep -q '[^[:space:]]' "$reviewer_output"; then
status=1
elif ! reviewer_output_has_clean_marker "$reviewer_output"; then
status=1
fi
fi
rm -f "$reviewer_output"
rm -f "$prompt_file"
prompt_file=
return "$status"
}
run_selected_review() {
local selected=$1
case "$selected" in
codex)
if ! command -v "$codex_bin" >/dev/null 2>&1; then
echo "codex reviewer unavailable: $codex_bin" >&2
return 127
fi
run_review
;;
claude|pi|opencode|droid|copilot)
run_prompt_reviewer "$selected"
;;
*)
echo "unsupported reviewer: $selected" >&2
return 2
;;
esac
}
fallback_reviewer_is_available() {
local selected=$1
case "$selected" in
claude) command -v "$claude_bin" >/dev/null 2>&1 ;;
pi) command -v "$pi_bin" >/dev/null 2>&1 ;;
opencode) command -v "$opencode_bin" >/dev/null 2>&1 ;;
droid) command -v "$droid_bin" >/dev/null 2>&1 ;;
copilot) command -v "$copilot_bin" >/dev/null 2>&1 ;;
*) return 1 ;;
esac
}
run_auto_fallback_review() {
local selected
if [[ "$fallback_reviewer" != auto ]]; then
run_selected_review "$fallback_reviewer"
return $?
fi
for selected in claude pi opencode droid copilot; do
if fallback_reviewer_is_available "$selected"; then
run_selected_review "$selected"
return $?
fi
done
echo "fallback reviewer unavailable: no configured fallback CLI found" >&2
return 127
}
run_auto_review() {
run_selected_review codex
local status=$?
if [[ "$status" == 0 ]]; then
return 0
fi
if (( status > 128 && status < 192 )); then
return "$status"
fi
if review_output_has_findings; then
return "$status"
fi
if [[ "$fallback_reviewer" == none ]]; then
return "$status"
fi
if [[ "$fallback_reviewer" == auto ]]; then
printf 'autoreview warning: codex exited %s; trying configured fallback reviewers\n' "$status" >&2
else
printf 'autoreview warning: codex exited %s; falling back to %s\n' "$status" "$fallback_reviewer" >&2
fi
run_auto_fallback_review
}
elapsed_since() {
local started_at=$1
local finished_at
finished_at=$(date +%s)
printf '%s\n' "$((finished_at - started_at))"
}
format_elapsed() {
local seconds=$1
if (( seconds < 60 )); then
printf '%ss\n' "$seconds"
else
printf '%sm%ss\n' "$((seconds / 60))" "$((seconds % 60))"
fi
}
review_output_empty() {
[[ ! -s "$review_output" ]] || ! grep -q '[^[:space:]]' "$review_output"
}
review_findings_text() {
if grep -Fxq 'codex' "$review_output"; then
awk '
$0 == "codex" {
capture = 1
output = $0 ORS
next
}
capture {
output = output $0 ORS
}
END {
printf "%s", output
}
' "$review_output"
return
fi
cat "$review_output"
}
review_output_has_findings() {
review_findings_text | grep -Eq '\[P[0-3]\]'
}
report_clean_review_or_fail() {
local elapsed_text
elapsed_text=$(format_elapsed "${review_elapsed_seconds:-0}")
if review_output_has_findings; then
printf 'autoreview complete after %s\n' "$elapsed_text"
printf 'autoreview findings: accepted/actionable findings reported\n'
return 1
fi
if review_output_empty; then
printf 'autoreview complete after %s; no output\n' "$elapsed_text"
return 1
fi
printf 'autoreview complete after %s\n' "$elapsed_text"
printf 'autoreview clean: no accepted/actionable findings reported\n'
}
if [[ -z "$parallel_tests" ]]; then
review_started_at=$(date +%s)
set +e
if [[ "$reviewer" == auto ]]; then
run_auto_review
else
run_selected_review "$reviewer"
fi
review_status=$?
review_elapsed_seconds=$(elapsed_since "$review_started_at")
set -e
if [[ "$review_status" == 0 ]]; then
report_clean_review_or_fail
exit $?
fi
exit "$review_status"
fi
review_status_file=$(mktemp)
review_elapsed_file=$(mktemp)
tests_status_file=$(mktemp)
(
set +e
review_started_at=$(date +%s)
if [[ "$reviewer" == auto ]]; then
run_auto_review
else
run_selected_review "$reviewer"
fi
status=$?
elapsed=$(elapsed_since "$review_started_at")
printf '%s\n' "$status" > "$review_status_file"
printf '%s\n' "$elapsed" > "$review_elapsed_file"
) &
review_pid=$!
(
set +e
bash -lc "$parallel_tests"
status=$?
printf '%s\n' "$status" > "$tests_status_file"
) &
tests_pid=$!
wait "$review_pid" || true
wait "$tests_pid" || true
review_status=$(cat "$review_status_file")
review_elapsed_seconds=$(cat "$review_elapsed_file")
tests_status=$(cat "$tests_status_file")
rm -f "$review_status_file" "$review_elapsed_file" "$tests_status_file"
printf 'autoreview exit: %s\n' "$review_status"
printf 'tests exit: %s\n' "$tests_status"
if [[ "$review_status" != 0 || "$tests_status" != 0 ]]; then
exit 1
fi
report_clean_review_or_fail

View File

@@ -1,103 +0,0 @@
---
name: codex-review
description: "Codex code review closeout: local dirty changes, PR branch vs main, parallel tests."
---
# Codex Review
Run Codex's built-in code review as a closeout check. This is code review (`codex review`), not Guardian `auto_review` approval routing.
Use when:
- user asks for Codex review / autoreview / second-model review
- after non-trivial code edits, before final/commit/ship
- reviewing a local branch or PR branch after fixes
## Contract
- Treat review output as advisory. Never blindly apply it.
- Verify every finding by reading the real code path and adjacent files.
- 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 Codex review returns no accepted/actionable findings.
- If a review-triggered fix changes code, rerun focused tests and rerun Codex review.
- 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.
- Do not push just to review. Push only when the user requested push/ship/PR update.
## Pick Target
Dirty local work:
```bash
codex review --uncommitted
```
Branch/PR work:
```bash
git fetch origin
codex review --base origin/main
```
Do not pass an inline prompt with `--base`; current CLI rejects `--base` + `[PROMPT]` even though help text is ambiguous. If custom instructions are needed, run the plain base review first, then do a local/manual follow-up pass.
If an open PR exists, use its actual base:
```bash
base=$(gh pr view --json baseRefName --jq .baseRefName)
codex review --base "origin/$base"
```
Committed single change:
```bash
codex review --commit HEAD
```
## Parallel Closeout
Format first if formatting can change line locations. Then it is OK to run tests and review in parallel:
```bash
scripts/codex-review --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.
## 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.
## Helper
Bundled helper:
```bash
~/.codex/skills/codex-review/scripts/codex-review --help
```
If installed from `agent-scripts`, path is:
```bash
/Users/steipete/Projects/agent-scripts/skills/codex-review/scripts/codex-review --help
```
The helper:
- chooses dirty `--uncommitted` first
- otherwise uses current PR base if `gh pr view` works
- otherwise uses `origin/main` for non-main branches
- writes only to stdout unless `--output` or `CODEX_REVIEW_OUTPUT` is set
- supports `--dry-run` and `--parallel-tests`
## Final Report
Include:
- review command used
- tests/proof run
- findings accepted/rejected, briefly why
- final clean review command, or why a remaining finding was consciously rejected

View File

@@ -1,188 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'EOF'
Usage: codex-review [options]
Options:
--mode auto|local|branch Target selection. Default: auto.
--base REF Base ref for branch review. Default: PR base or origin/main.
--codex-bin PATH Codex binary. Default: codex.
--output FILE Also save output to file.
--parallel-tests CMD Run review and test command concurrently.
--dry-run Print selected commands, do not run.
-h, --help Show help.
Modes:
local codex review --uncommitted
branch codex review --base <base>
auto dirty tree -> local, else PR/current branch -> branch
EOF
}
mode=auto
base_ref=
codex_bin=${CODEX_BIN:-codex}
output=${CODEX_REVIEW_OUTPUT:-}
parallel_tests=
dry_run=false
while [[ $# -gt 0 ]]; do
case "$1" in
--mode)
mode=${2:-}
shift 2
;;
--base)
base_ref=${2:-}
shift 2
;;
--codex-bin)
codex_bin=${2:-}
shift 2
;;
--output)
output=${2:-}
shift 2
;;
--parallel-tests)
parallel_tests=${2:-}
shift 2
;;
--dry-run)
dry_run=true
shift
;;
-h|--help)
usage
exit 0
;;
*)
usage >&2
exit 2
;;
esac
done
case "$mode" in
auto|local|branch) ;;
*)
echo "invalid --mode: $mode" >&2
exit 2
;;
esac
git rev-parse --show-toplevel >/dev/null
current_branch=$(git branch --show-current 2>/dev/null || true)
dirty=false
if [[ -n "$(git status --porcelain)" ]]; then
dirty=true
fi
pr_url=
if [[ -z "$base_ref" && "$mode" != local ]] && command -v gh >/dev/null 2>&1; then
if pr_lines=$(gh pr view --json baseRefName,url --jq '[.baseRefName, .url] | @tsv' 2>/dev/null); then
base_name=${pr_lines%%$'\t'*}
pr_url=${pr_lines#*$'\t'}
if [[ -n "$base_name" ]]; then
base_ref="origin/$base_name"
fi
fi
fi
if [[ -z "$base_ref" ]]; then
base_ref=origin/main
fi
review_kind=
if [[ "$mode" == local || ( "$mode" == auto && "$dirty" == true ) ]]; then
review_kind=local
elif [[ "$mode" == branch || ( "$mode" == auto && -n "$current_branch" && "$current_branch" != "main" ) ]]; then
review_kind=branch
else
echo "no review target: clean main checkout and no forced mode" >&2
exit 1
fi
if [[ "$review_kind" == local ]]; then
review_cmd=("$codex_bin" review --uncommitted)
else
review_cmd=("$codex_bin" review --base "$base_ref")
fi
printf 'codex-review target: %s\n' "$review_kind"
printf 'branch: %s\n' "${current_branch:-detached}"
if [[ -n "$pr_url" ]]; then
printf 'pr: %s\n' "$pr_url"
fi
printf 'review:'
printf ' %q' "${review_cmd[@]}"
printf '\n'
if [[ -n "$parallel_tests" ]]; then
printf 'tests: %s\n' "$parallel_tests"
fi
if [[ "$review_kind" == branch ]]; then
printf 'fetch: git fetch origin --quiet\n'
fi
if [[ -n "$output" ]]; then
printf 'output: %s\n' "$output"
fi
if [[ "$dry_run" == true ]]; then
exit 0
fi
if [[ "$review_kind" == branch ]]; then
git fetch origin --quiet || {
echo "warning: git fetch origin failed; reviewing with existing refs" >&2
}
fi
run_review() {
if [[ -n "$output" ]]; then
mkdir -p "$(dirname "$output")"
"${review_cmd[@]}" 2>&1 | tee "$output"
else
"${review_cmd[@]}"
fi
}
if [[ -z "$parallel_tests" ]]; then
run_review
exit $?
fi
review_status_file=$(mktemp)
tests_status_file=$(mktemp)
(
set +e
run_review
status=$?
printf '%s\n' "$status" > "$review_status_file"
) &
review_pid=$!
(
set +e
bash -lc "$parallel_tests"
status=$?
printf '%s\n' "$status" > "$tests_status_file"
) &
tests_pid=$!
wait "$review_pid" || true
wait "$tests_pid" || true
review_status=$(cat "$review_status_file")
tests_status=$(cat "$tests_status_file")
rm -f "$review_status_file" "$tests_status_file"
printf 'codex-review exit: %s\n' "$review_status"
printf 'tests exit: %s\n' "$tests_status"
if [[ "$review_status" != 0 || "$tests_status" != 0 ]]; then
exit 1
fi

View File

@@ -1,23 +1,32 @@
---
name: crabbox
description: Use Crabbox for OpenClaw remote validation across Linux, macOS, Windows, and WSL2. Default to the repo Crabbox config, use brokered AWS for normal broad proof, and keep Blacksmith Testbox as an explicit opt-in or outage diagnostic path.
description: Use the Crabbox wrapper for OpenClaw remote validation across Linux, macOS, Windows, and WSL2, including delegated Blacksmith Testbox proof. Report the actual provider and id.
---
# Crabbox
Use Crabbox when OpenClaw needs remote Linux proof for broad tests, CI-parity
checks, secrets, hosted services, Docker/E2E/package lanes, warmed reusable
boxes, sync timing, logs/results, cache inspection, or lease cleanup.
Use the Crabbox wrapper when OpenClaw needs remote Linux proof for broad tests,
CI-parity checks, secrets, hosted services, Docker/E2E/package lanes, warmed
reusable boxes, sync timing, logs/results, cache inspection, or lease cleanup.
Default backend: the repo `.crabbox.yaml`, currently brokered AWS. Do not
override it to Blacksmith unless the user explicitly asks for Blacksmith proof,
the task is specifically about Testbox behavior, or AWS/brokered Crabbox is the
broken layer.
Crabbox is the transport/orchestration surface. The actual backend can be:
Blacksmith Testbox is a delegated fallback, not the default router. If a
Blacksmith run queues, fails capacity, fails auth, or cannot allocate, stop
after one real attempt and switch to the repo default or report the blocker.
Do not retry Blacksmith in a loop.
- brokered AWS Crabbox: direct provider, `provider=aws`, lease ids like
`cbx_...`, `syncDelegated=false`
- Blacksmith Testbox through Crabbox: delegated provider,
`provider=blacksmith-testbox`, ids like `tbx_...`, `syncDelegated=true`
For OpenClaw maintainer broad `pnpm` gates, Blacksmith Testbox through the
Crabbox wrapper is acceptable and often preferred when the standing Testbox
rules apply. Do not describe those runs as "AWS Crabbox"; report them as
Testbox-through-Crabbox with the `tbx_...` id and Actions run.
Use the repo `.crabbox.yaml` brokered AWS path when the task specifically needs
direct AWS Crabbox behavior, persistent direct-provider leases, `--fresh-pr`,
`--full-resync`, environment forwarding, capture/download support, or provider
comparison. Use `--provider blacksmith-testbox` when the task needs OpenClaw
maintainer Testbox proof, prepared CI environment, broad/heavy pnpm gates, or
the user asks for Testbox/Blacksmith.
## First Checks
@@ -34,10 +43,15 @@ pnpm crabbox:run -- --help | sed -n '1,120p'
- OpenClaw scripts prefer `../crabbox/bin/crabbox` when present. The user PATH
shim can be stale.
- Check `.crabbox.yaml` for repo defaults and honor them. For normal Linux
validation, omit `--provider` so the wrapper uses brokered AWS.
- Pass `--provider blacksmith-testbox` only for explicit Blacksmith/Testbox
work or a deliberate comparison.
- Check `.crabbox.yaml` for direct-provider defaults. Omitting `--provider`
means brokered AWS today.
- For broad OpenClaw maintainer `pnpm` gates, prefer the repo wrapper with
`--provider blacksmith-testbox` or the repo Testbox helpers when the standing
Testbox policy applies.
- Always report the actual provider and id. `cbx_...` means AWS Crabbox;
`tbx_...` means Blacksmith Testbox through Crabbox. If the output only says
`blacksmith testbox list`, use `blacksmith testbox list --all` before
concluding no box exists.
- If a warm direct-provider lease smells stale, retry with `--full-resync`
(alias `--fresh-sync`) before replacing the lease. This resets the remote
workdir, skips the fingerprint fast path, reseeds Git when possible, and
@@ -64,6 +78,22 @@ Use these only when the task needs an existing non-Linux host. OpenClaw broad
Linux validation uses the repo Crabbox config unless a provider is explicitly
requested.
When the user explicitly asks for brokered macOS runners, use Crabbox AWS
macOS only after confirming the deployed coordinator supports EC2 Mac host
lifecycle/image routes and the operator has AWS EC2 Mac Dedicated Host quota
and IAM. Prefer `CRABBOX_HOST_ID` for a known Crabbox-managed Dedicated Host,
or run the no-spend preflight first:
```sh
crabbox admin hosts quota --provider aws --target macos --region eu-west-1 --type mac2.metal --json
crabbox admin hosts allocate --provider aws --target macos --region eu-west-1 --type mac2.metal --dry-run --json
CRABBOX_MACOS_TYPES=all scripts/macos-host-region-preflight.sh
```
Do not silently substitute AWS macOS for normal OpenClaw Linux proof. Report
paid-host blockers as quota, IAM, coordinator deployment, or host availability
instead of falling back to local macOS.
Crabbox supports static SSH targets:
```sh
@@ -83,11 +113,10 @@ Crabbox supports static SSH targets:
with `../crabbox/bin/crabbox run --help`, config/flag tests, and the Crabbox
Go test suite.
## Default Brokered AWS Backend
## Direct Brokered AWS Backend
Use this for `pnpm check`, `pnpm check:changed`, `pnpm test`,
`pnpm test:changed`, Docker/E2E/live/package gates, or anything likely to fan
out across many Vitest projects.
Use this when the task needs direct AWS Crabbox semantics rather than the
prepared Blacksmith Testbox CI environment.
Changed gate:
@@ -124,9 +153,9 @@ pnpm crabbox:run -- \
Read the JSON summary. Useful fields:
- `provider`: should normally be `aws`
- `provider`: `aws`
- `leaseId`: `cbx_...`
- `syncDelegated`: should normally be `false`
- `syncDelegated`: `false`
- `commandPhases`: populated when the command prints `CRABBOX_PHASE:<name>`
- `commandMs` / `totalMs`
- `exitCode`
@@ -138,6 +167,41 @@ cleanup when a run fails, is interrupted, or the command output is unclear:
../crabbox/bin/crabbox list --provider aws
```
## Blacksmith Testbox Through Crabbox
Use this for OpenClaw maintainer broad/heavy `pnpm` gates when the prepared CI
environment is the right proof surface:
```sh
node scripts/crabbox-wrapper.mjs run \
--provider blacksmith-testbox \
--blacksmith-org openclaw \
--blacksmith-workflow .github/workflows/ci-check-testbox.yml \
--blacksmith-job check \
--blacksmith-ref main \
--idle-timeout 90m \
--ttl 240m \
--timing-json \
-- \
CI=1 NODE_OPTIONS=--max-old-space-size=4096 OPENCLAW_TEST_PROJECTS_PARALLEL=6 OPENCLAW_VITEST_MAX_WORKERS=1 OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS=900000 OPENCLAW_TESTBOX=1 OPENCLAW_TESTBOX_REMOTE_RUN=1 pnpm check:changed
```
Read the JSON summary and the Testbox line. Useful fields:
- `provider`: `blacksmith-testbox`
- `leaseId`: `tbx_...`
- `syncDelegated`: `true`
- `syncPhases`: delegated/skipped because Blacksmith owns checkout/sync
- Actions run URL/id from the Testbox output
- `exitCode`
`blacksmith testbox list` may hide hydrating or ready boxes. Use:
```sh
blacksmith testbox list --all
blacksmith testbox status <tbx_id>
```
## Observability Flags
Use these on debugging runs before inventing ad hoc logging:
@@ -223,6 +287,13 @@ Use the smallest Crabbox lane that proves the reported user path, not just the
touched code. Aim for one after-fix E2E proof before commenting, closing, or
opening a PR for a user-visible bug.
When the user says "test in Crabbox", do not simply copy tests to the remote
box and run them there. Crabbox is for remote real-scenario proof: copy or
install OpenClaw as the user would, run the same setup/update/CLI/Gateway/API
call that failed, and capture behavior from that entrypoint. For regressions or
bug reports, prove the broken state first when feasible, then run the same
scenario after the fix.
Pick the lane by symptom:
- Docker/setup/install bug: build a package tarball and run the matching
@@ -244,8 +315,9 @@ Pick the lane by symptom:
Efficient flow:
1. Reproduce or prove the pre-fix symptom when feasible. If the issue cannot be
reproduced, capture the exact command and observed behavior instead.
1. Reproduce or prove the pre-fix symptom from the real user-facing entrypoint
when feasible. If the issue cannot be reproduced, capture the exact command
and observed behavior instead.
2. Patch locally and run narrow local tests for edit speed.
3. Run one Crabbox E2E command that starts from the user-facing entrypoint:
package install, Docker setup, onboarding, channel add, gateway start, or
@@ -353,18 +425,18 @@ Common desktop flow:
```sh
../crabbox/bin/crabbox warmup --provider hetzner --desktop --browser --class standard --idle-timeout 60m --ttl 240m
../crabbox/bin/crabbox desktop launch --provider hetzner --id <cbx_id-or-slug> --browser --url https://example.com --webvnc --open
../crabbox/bin/crabbox desktop launch --provider hetzner --id <cbx_id-or-slug> --browser --url https://example.com --webvnc --open --take-control
```
Useful WebVNC commands:
```sh
../crabbox/bin/crabbox webvnc --provider hetzner --id <cbx_id-or-slug> --open
../crabbox/bin/crabbox webvnc daemon start --provider hetzner --id <cbx_id-or-slug> --open
../crabbox/bin/crabbox webvnc --provider hetzner --id <cbx_id-or-slug> --open --take-control
../crabbox/bin/crabbox webvnc daemon start --provider hetzner --id <cbx_id-or-slug> --open --take-control
../crabbox/bin/crabbox webvnc daemon status --provider hetzner --id <cbx_id-or-slug>
../crabbox/bin/crabbox webvnc daemon stop --provider hetzner --id <cbx_id-or-slug>
../crabbox/bin/crabbox webvnc status --provider hetzner --id <cbx_id-or-slug>
../crabbox/bin/crabbox webvnc reset --provider hetzner --id <cbx_id-or-slug> --open
../crabbox/bin/crabbox webvnc reset --provider hetzner --id <cbx_id-or-slug> --open --take-control
../crabbox/bin/crabbox desktop doctor --provider hetzner --id <cbx_id-or-slug>
../crabbox/bin/crabbox desktop click --provider hetzner --id <cbx_id-or-slug> --x 640 --y 420
../crabbox/bin/crabbox desktop paste --provider hetzner --id <cbx_id-or-slug> --text "user@example.com"
@@ -377,6 +449,32 @@ Useful WebVNC commands:
browser/app inside the visible session, bridges the lease into the authenticated
WebVNC portal, and opens the portal. Keep browsers windowed for human QA; use
`--fullscreen` only for capture/video workflows.
For human handoff, include `--take-control` so the opened portal viewer gets
keyboard/mouse control automatically instead of landing as an observer.
Human handoff preflight:
- Do not assume a visible desktop or launched browser means the repo CLI/app is
installed, built, or on the interactive terminal's `PATH`.
- Before handing WebVNC to a human tester, prove the expected command from the
same kept lease and from a neutral directory such as `~`.
- If the handoff needs repo-local code, sync/build/link it explicitly on that
lease. Source-tree CLIs often need build output before a symlink works.
- Prefer a real `command -v <expected-command> && <expected-command> --version`
check over a repo-root-only `pnpm ...` command.
Generic handoff repair pattern:
```sh
../crabbox/bin/crabbox run --id <cbx_id-or-slug> --full-resync --shell -- \
"set -euo pipefail
pnpm install --frozen-lockfile
pnpm build
sudo ln -sf \"\$PWD/<cli-entry>\" /usr/local/bin/<expected-command>
cd ~
command -v <expected-command>
<expected-command> --version"
```
## If Crabbox Fails

View File

@@ -0,0 +1,44 @@
---
name: discrawl
description: "Discord archive: search, sync freshness, DMs, channel slices, SQL counts, and Discrawl repo work."
metadata:
openclaw:
homepage: https://github.com/openclaw/discrawl
requires:
bins:
- discrawl
install:
- kind: go
module: github.com/openclaw/discrawl/cmd/discrawl@latest
bins:
- discrawl
---
# Discrawl
Use local Discord archive data before live Discord APIs. Check freshness for recent/current questions:
```bash
discrawl status --json
discrawl doctor
```
Refresh only when stale or asked:
```bash
discrawl sync --source wiretap
discrawl sync
```
Query with bounded slices:
```bash
DISCRAWL_NO_AUTO_UPDATE=1 discrawl search --limit 20 "query"
discrawl messages --channel '#maintainers' --days 7 --all
discrawl dms --last 20
DISCRAWL_NO_AUTO_UPDATE=1 discrawl --json sql "select count(*) from messages;"
```
Report absolute date spans, channel/DM names, counts, and known gaps. Use read-only SQL for exact counts/rankings. Never use `--unsafe --confirm` unless the user explicitly requests a reviewed DB mutation.
Boundaries: bot sync needs configured Discord bot credentials. Wiretap reads local Discord Desktop artifacts only; do not extract user tokens, call Discord as the user, or write to Discord storage. Git-share snapshots must not include secrets or `@me` DM rows.

View File

@@ -0,0 +1,4 @@
interface:
display_name: "Discrawl"
short_description: "Search local Discord archives and freshness"
default_prompt: "Use $discrawl to search local Discord archives, check freshness, inspect DMs or channel slices, and report exact date spans and source gaps."

View File

@@ -1,68 +1,50 @@
---
name: gitcrawl
description: Use gitcrawl for OpenClaw issue and PR archive search, duplicate discovery, related-thread clustering, and local GitHub mirror freshness checks.
description: "GitHub archive: issue/PR search, sync freshness, duplicate clusters, gh-shim PR status, and Gitcrawl repo work."
metadata:
openclaw:
homepage: https://github.com/openclaw/gitcrawl
requires:
bins:
- gitcrawl
install:
- kind: go
module: github.com/openclaw/gitcrawl/cmd/gitcrawl@latest
bins:
- gitcrawl
---
# Gitcrawl
Use this skill before live GitHub search when triaging OpenClaw issues or PRs.
`gitcrawl` is the local candidate-discovery layer. It is fast, includes open and closed threads, and can surface duplicate attempts, related issues, and already-landed fixes. It is not the final source of truth for comments, labels, merges, closes, or current CI.
## Default Flow
1. Check local state:
Use local GitHub issue/PR archives before live GitHub search. Check freshness first:
```bash
gitcrawl doctor --json
```
2. Read the target from the local archive:
Find candidates:
```bash
gitcrawl threads openclaw/openclaw --numbers <issue-or-pr-number> --include-closed --json
```
3. Find related candidates:
```bash
gitcrawl neighbors openclaw/openclaw --number <issue-or-pr-number> --limit 12 --json
gitcrawl search openclaw/openclaw --query "<scope or title keywords>" --mode hybrid --limit 20 --json
gitcrawl search issues "query" -R openclaw/openclaw --state open --json number,title,url
gitcrawl clusters openclaw/openclaw --sort size --min-size 5
gitcrawl cluster-detail openclaw/openclaw --id <cluster-id>
```
4. Inspect relevant clusters:
For PR triage, start cached and go live only before mutation/merge decisions:
```bash
gitcrawl cluster-detail openclaw/openclaw --id <cluster-id> --member-limit 20 --body-chars 280 --json
gitcrawl gh pr status <number-or-url> -R openclaw/openclaw --compact
gitcrawl gh pr view <number-or-url> -R openclaw/openclaw --json number,title,state,url,isDraft,headRef,headSha
gitcrawl gh --live pr status <number-or-url> -R openclaw/openclaw --compact
```
5. Verify anything actionable with live GitHub and the checkout:
Use live `gh` plus checkout proof before commenting, labeling, closing, reopening, merging, or filing a PR review:
```bash
gh pr view <number> --json number,title,state,mergedAt,body,files,comments,reviews,statusCheckRollup
gh issue view <number> --json number,title,state,body,comments,closedAt
```
## Freshness Rules
- Treat `gitcrawl` as stale if `doctor` shows no target thread, an old `last_sync_at`, missing embeddings for neighbor/search commands, or a clearly wrong open/closed state.
- If stale data blocks the decision, refresh the portable store first:
```bash
gitcrawl init --portable-store git@github.com:openclaw/gitcrawl-store.git --json
```
- Run expensive update commands such as `gitcrawl sync --include-comments` only when the user asked to update the local store or stale data is blocking the decision.
- The sync default is all GitHub thread states; pass `--state open`, `--state closed`, or `--state all` only when a task requires a narrower or explicit scope.
## Boundaries
- Use `gitcrawl` for candidates, clusters, and historical context.
- Use `gh`, `gh api`, and the current checkout for live state before commenting, labeling, closing, reopening, merging, or filing a PR review.
- Do not close or label based only on `gitcrawl` similarity. Require matching problem intent plus live verification.
- If `gitcrawl` is unavailable, say so and fall back to targeted `gh search` rather than blocking normal maintainer work.
Report absolute dates, repo names, issue/PR numbers, cluster ids, and source gaps. Do not close/label from similarity alone; require matching intent plus live verification.

View File

@@ -0,0 +1,44 @@
---
name: graincrawl
description: "Granola archive: search, sync freshness, notes, transcripts, panels, SQL counts, and Graincrawl repo work."
metadata:
openclaw:
homepage: https://github.com/openclaw/graincrawl
requires:
bins:
- graincrawl
install:
- kind: go
module: github.com/vincentkoc/graincrawl/cmd/graincrawl@latest
bins:
- graincrawl
---
# Graincrawl
Use local Granola archive data first. Check freshness for recent/current questions:
```bash
graincrawl doctor --json
graincrawl status --json
```
Refresh only when stale or asked:
```bash
graincrawl sync --source private-api
graincrawl sync --source desktop-cache
```
Query with bounded reads:
```bash
graincrawl search "query"
graincrawl notes --json
graincrawl note get <id>
graincrawl transcripts get <id>
graincrawl panels get <id>
graincrawl --json sql "select count(*) as notes from notes;"
```
Report absolute date spans, note titles, source gaps, and transcript/panel availability. Use read-only SQL for exact counts/rankings. Before encrypted source debugging, run explicit unlock/secrets checks; do not surprise-prompt Keychain.

View File

@@ -0,0 +1,4 @@
interface:
display_name: "Graincrawl"
short_description: "Search local Granola notes and transcripts"
default_prompt: "Use $graincrawl to search local Granola notes, transcripts, and panels, check freshness, and report exact date spans and source gaps."

View File

@@ -0,0 +1,42 @@
---
name: notcrawl
description: "Notion archive: search, sync freshness, pages/databases, Markdown exports, SQL counts, and Notcrawl repo work."
metadata:
openclaw:
homepage: https://github.com/openclaw/notcrawl
requires:
bins:
- notcrawl
install:
- kind: go
module: github.com/vincentkoc/notcrawl/cmd/notcrawl@latest
bins:
- notcrawl
---
# Notcrawl
Use local Notion archive data before browsing or live Notion API calls. Check freshness for recent/current questions:
```bash
notcrawl doctor
notcrawl status --json
```
Refresh only when stale or asked:
```bash
notcrawl sync --source desktop
notcrawl sync --source api
```
Query with bounded reads:
```bash
notcrawl search "query"
notcrawl databases
notcrawl report
notcrawl sql "select count(*) from pages;"
```
Report workspace/teamspace, page/database titles, absolute date spans, counts, and known gaps. Use read-only SQL only; never mutate the archive. API mode requires `NOTION_TOKEN`; do not assume token availability.

View File

@@ -0,0 +1,4 @@
interface:
display_name: "Notcrawl"
short_description: "Search local Notion archives and freshness"
default_prompt: "Use $notcrawl to search local Notion pages and databases, check freshness, inspect exports, and report exact date spans and source gaps."

View File

@@ -24,6 +24,36 @@ gitcrawl search openclaw/openclaw --query "<scope or title keywords>" --mode hyb
gitcrawl cluster-detail openclaw/openclaw --id <cluster-id> --member-limit 20 --body-chars 280 --json
```
## Claim specific review targets
When a maintainer asks Codex to review, triage, fix, or land a specific OpenClaw issue/PR, check assignment before deep work.
- Identify the requesting maintainer's GitHub login. In this environment, default Peter to `steipete`; if another maintainer is clearly the requester, use that maintainer's bare login.
- Read current assignees with live `gh issue view` / `gh pr view`; `gitcrawl` is not enough for assignment state.
- If unassigned, assign the requester before deep review. This is allowed for specific requested targets; do not auto-assign broad discovery candidates or shortlists.
- If assigned to someone else, say so clearly before analysis and include assignment age:
- fresh: assigned within 6h; treat as actively owned unless user explicitly asks to continue or reassign
- stale: assigned 6h+ ago; treat as ownership hint, not a hard block; continue only with that caveat
- If assigned to requester plus others, mention co-assignees and continue.
- If assignment event time is unavailable, say `assigned, time unknown`; treat as assigned, not stale.
- Never remove or replace assignees unless explicitly asked.
Assignment time proof:
```bash
gh api "repos/openclaw/openclaw/issues/<number>/timeline" --paginate \
-H "Accept: application/vnd.github+json" \
--jq '[.[] | select(.event=="assigned") | {assignee:.assignee.login, assigner:.assigner.login, actor:.actor.login, created_at}]'
```
Use the newest `assigned` event for each current assignee. Issue timeline events expose `created_at`; GitHub GraphQL `AssignedEvent.createdAt` is also valid when REST pagination is awkward.
Claim command for issues or PRs:
```bash
gh api -X POST "repos/openclaw/openclaw/issues/<number>/assignees" -f 'assignees[]=<login>' >/dev/null
```
## Surface opener identity
- For every reviewed, triaged, closed, or landed issue/PR, show the opener's human name when available, GitHub login, and account age.
@@ -217,6 +247,7 @@ gh search issues --repo openclaw/openclaw --match title,body --limit 50 \
not correctness findings.
- If bot review conversations exist on your PR, address them and resolve them yourself once fixed.
- Leave a review conversation unresolved only when reviewer or maintainer judgment is still needed.
- Before landing any PR with non-trivial code changes, run `$autoreview` until no accepted/actionable findings remain, unless equivalent manual review already covered it, the change is trivial/docs-only, or the user opts out.
- When landing or merging any PR, follow the global `/landpr` process.
- Use `scripts/committer "<msg>" <file...>` for scoped commits instead of manual `git add` and `git commit`.
- Keep commit messages concise and action-oriented.

View File

@@ -34,10 +34,10 @@ Supports single or multiple alerts. For multiple alerts, process in ascending or
For each alert:
1. **Identify**`fetch-alert` + `fetch-content` to get metadata and body
2. **Decide** — Agent reads the body file, identifies all secrets, produces redacted version
3. **Redact**`redact-body` for issue/PR body; skip for comments (delete directly)
2. **Decide** — Agent reads the body file, identifies whether plaintext secrets remain, and produces a redacted version only when needed
3. **Redact**`redact-body-if-needed` for issue/PR body; skip for comments (delete directly)
4. **Purge**`delete-comment` + `recreate-comment` for comments; cannot purge body history
5. **Notify**`notify` posts the right template per location type
5. **Notify**`notify` posts the right template per location type, unless the current issue/PR body is already redacted
6. **Resolve**`resolve` closes the alert
7. **Summary**`summary` prints formatted results
@@ -81,11 +81,20 @@ The `fetch-content` output includes:
The agent reads the body file from `fetch-content` output and:
1. Identifies ALL secrets in the content (there may be more than the alert flagged)
2. Replaces each secret with `[REDACTED <secret_type>]`**no partial values, no prefix/suffix**
3. Saves the redacted content to a new temp file
2. Determines whether any plaintext credential remains in the current body
3. Replaces each remaining secret with `[REDACTED <secret_type>]`**no partial values, no prefix/suffix**
4. Saves the redacted content to a new temp file
This is the only step that requires semantic understanding. Everything else is mechanical.
For `issue_body` and `pull_request_body`: if the current body has already been redacted by the author and no plaintext credential remains, **do not post a public notification comment**. Resolve the alert with a maintainer-only resolution comment such as:
```bash
node secret-scanning.mjs resolve <ALERT_NUMBER> revoked "Current issue/PR body is already redacted; no public notification posted."
```
This avoids creating a fresh public pointer to historical sensitive content.
## Step 3: Redact
### For comments (issue_comment / PR comments)
@@ -95,9 +104,11 @@ This is the only step that requires semantic understanding. Everything else is m
### For issue_body / pull_request_body
```bash
node secret-scanning.mjs redact-body <issue|pr> <NUMBER> <redacted-body-file>
node secret-scanning.mjs redact-body-if-needed <issue|pr> <NUMBER> <current-body-file> <redacted-body-file> <result-file>
```
Use the `body_file` from `fetch-content` as `<current-body-file>`. The command writes `notify_required` to `<result-file>` and only PATCHes the body when the redacted file differs from the current body.
## Step 4: Purge Edit History
### Comments — Delete and Recreate
@@ -134,10 +145,12 @@ The recreated comment should follow this format:
<redacted original content>
```
### issue_body / pull_request_body — Cannot Purge
### issue_body / pull_request_body — Cannot Purge Edit History
Editing creates an edit history revision with the pre-edit plaintext. This cannot be cleared via API.
Do not advise authors publicly to delete/recreate issues or close/reopen PRs. That can draw attention to historical content. Keep purge guidance maintainer-only.
**Output to maintainer terminal only (never in public comments):**
```
@@ -155,12 +168,13 @@ Cannot clean. Notify author to delete branch or force-push (for unmerged PRs).
## Step 5: Notify
```bash
node secret-scanning.mjs notify <TARGET> <AUTHOR> <LOCATION_TYPE> <SECRET_TYPES> [REPLY_TO_NODE_ID]
node secret-scanning.mjs notify <TARGET> <AUTHOR> <LOCATION_TYPE> <SECRET_TYPES> [REPLY_TO_NODE_ID|BODY_REDACTION_RESULT_FILE]
```
- For non-discussion types, `<TARGET>` is the issue/PR number.
- For `discussion_comment`, `<TARGET>` is the `discussion_node_id` returned by `fetch-content`.
- For reply-style `discussion_comment` locations, pass the optional `reply_to_node_id` from `fetch-content` so the notification stays in the same thread.
- For `issue_body` and `pull_request_body`, pass the `<result-file>` from `redact-body-if-needed`. The script skips notification when `notify_required` is `false` and refuses body notifications without this file.
Secret types are comma-separated: `"Discord Bot Token,Feishu App Secret"`
@@ -170,6 +184,8 @@ The script picks the right template:
- **body types**: "your issue/PR description … redacted in place"
- **commit**: "code you committed"
For `issue_body` and `pull_request_body`, only notify when the current body still contained plaintext and maintainers redacted it. If the user already redacted the current body, skip this step and resolve silently.
## Step 6: Resolve
```bash
@@ -178,7 +194,7 @@ node secret-scanning.mjs resolve <ALERT_NUMBER>
node secret-scanning.mjs resolve <ALERT_NUMBER> revoked "Custom comment"
```
Resolution is `revoked` by default. As maintainers we cannot control whether users rotate — our responsibility is to redact + notify. The `revoked` means "this secret should be considered leaked", not "I confirmed it was revoked".
Resolution is `revoked` by default. As maintainers we cannot control whether users rotate — our responsibility is to remove current plaintext exposure and notify only when public notification is useful. The `revoked` means "this secret should be considered leaked", not "I confirmed it was revoked".
## Step 7: Summary

View File

@@ -7,6 +7,7 @@ import crypto from "node:crypto";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { pathToFileURL } from "node:url";
const REPO = "openclaw/openclaw";
const REPO_URL = `https://github.com/${REPO}`;
@@ -50,6 +51,34 @@ function ghGraphQL(query, options = {}) {
return gh(["api", "graphql", "-f", `query=${query}`], options);
}
function isBodyLocationType(locationType) {
return locationType === "issue_body" || locationType === "pull_request_body";
}
export function decideBodyRedaction(currentBody, redactedBody) {
const bodyChanged = String(currentBody) !== String(redactedBody);
return {
body_changed: bodyChanged,
notify_required: bodyChanged,
};
}
export function loadBodyRedactionResult(locationType, resultFile) {
if (!isBodyLocationType(locationType)) {
return { notify_required: true };
}
if (!resultFile) {
fail("Body notifications require a redaction result file from redact-body-if-needed");
}
if (!fs.existsSync(resultFile)) fail(`File not found: ${resultFile}`);
const result = JSON.parse(fs.readFileSync(resultFile, "utf8"));
if (typeof result.notify_required !== "boolean") {
fail(`Invalid redaction result file: missing boolean notify_required in ${resultFile}`);
}
return result;
}
function failOnGraphQLFailure(result, message) {
if (result?.gh_failed) {
const details = (
@@ -470,6 +499,43 @@ function cmdRedactBody(kind, number, bodyFile) {
console.log(JSON.stringify({ ok: true, kind, number: Number(number) }));
}
/**
* redact-body-if-needed <issue|pr> <number> <current-body-file> <redacted-body-file> <result-file>
* PATCH only when the agent-produced redacted body differs from the current body.
*/
function cmdRedactBodyIfNeeded(kind, number, currentBodyFile, redactedBodyFile, resultFile) {
if (!kind || !number || !currentBodyFile || !redactedBodyFile || !resultFile) {
fail(
"Usage: redact-body-if-needed <issue|pr> <number> <current-body-file> <redacted-body-file> <result-file>",
);
}
if (!fs.existsSync(currentBodyFile)) fail(`File not found: ${currentBodyFile}`);
if (!fs.existsSync(redactedBodyFile)) fail(`File not found: ${redactedBodyFile}`);
const currentBody = fs.readFileSync(currentBodyFile, "utf8");
const redactedBody = fs.readFileSync(redactedBodyFile, "utf8");
const decision = decideBodyRedaction(currentBody, redactedBody);
const result = {
ok: true,
kind,
number: Number(number),
...decision,
};
if (decision.body_changed) {
const endpoint =
kind === "pr" ? `repos/${REPO}/pulls/${number}` : `repos/${REPO}/issues/${number}`;
gh(["api", endpoint, "-X", "PATCH", "-F", `body=@${redactedBodyFile}`]);
result.redacted = true;
} else {
result.redacted = false;
result.reason = "current_body_already_redacted";
}
fs.writeFileSync(resultFile, `${JSON.stringify(result, null, 2)}\n`, { mode: 0o600 });
console.log(JSON.stringify(result));
}
/**
* delete-comment <comment-id>
* Delete a comment (and all its edit history).
@@ -555,6 +621,17 @@ function cmdNotify(target, author, locationType, secretTypes, replyToNodeId) {
const types = secretTypes.split(",").map((s) => s.trim());
const typeList = types.map((t, i) => `${i + 1}. **${t}**`).join("\n");
const redactionResult = loadBodyRedactionResult(locationType, replyToNodeId);
if (isBodyLocationType(locationType) && !redactionResult.notify_required) {
console.log(
JSON.stringify({
ok: true,
skipped: true,
reason: "current_body_already_redacted",
}),
);
return;
}
let locationDesc;
let actionDesc;
@@ -758,12 +835,13 @@ function cmdSummary(jsonFile) {
// ─── Dispatch ───────────────────────────────────────────────────────────────
const [command, ...args] = process.argv.slice(2);
const args = [];
const commands = {
export const commands = {
"fetch-alert": () => cmdFetchAlert(args[0]),
"fetch-content": () => cmdFetchContent(args[0]),
"redact-body": () => cmdRedactBody(args[0], args[1], args[2]),
"redact-body-if-needed": () => cmdRedactBodyIfNeeded(args[0], args[1], args[2], args[3], args[4]),
"delete-comment": () => cmdDeleteComment(args[0]),
"delete-discussion-comment": () => cmdDeleteDiscussionComment(args[0]),
"recreate-comment": () => cmdRecreateComment(args[0], args[1]),
@@ -774,26 +852,37 @@ const commands = {
summary: () => cmdSummary(args[0]),
};
if (!command || !commands[command]) {
console.error(
[
"Usage: node secret-scanning.mjs <command> [args]",
"",
"Commands:",
" fetch-alert <number> Fetch alert metadata + locations",
" fetch-content '<location-json>' Fetch content for a location",
" redact-body <issue|pr> <n> <file> PATCH body with redacted file",
" delete-comment <comment-id> Delete a comment",
" delete-discussion-comment <node-id> Delete a discussion comment (GraphQL)",
" recreate-comment <issue-n> <file> Create replacement comment",
" recreate-discussion-comment <disc-node-id> <file> [reply-to-node-id] Create discussion comment (GraphQL)",
" notify <target> <author> <type> <types> [reply-to-node-id] Post notification",
" resolve <n> [resolution] [comment] Close alert",
" list-open List open alerts",
" summary <json-file> Print formatted summary",
].join("\n"),
);
process.exit(1);
function main(argv = process.argv.slice(2)) {
const [command, ...commandArgs] = argv;
args.length = 0;
args.push(...commandArgs);
if (!command || !commands[command]) {
console.error(
[
"Usage: node secret-scanning.mjs <command> [args]",
"",
"Commands:",
" fetch-alert <number> Fetch alert metadata + locations",
" fetch-content '<location-json>' Fetch content for a location",
" redact-body <issue|pr> <n> <file> PATCH body with redacted file",
" redact-body-if-needed <issue|pr> <n> <current-file> <redacted-file> <result-file> PATCH body only if redaction changed it",
" delete-comment <comment-id> Delete a comment",
" delete-discussion-comment <node-id> Delete a discussion comment (GraphQL)",
" recreate-comment <issue-n> <file> Create replacement comment",
" recreate-discussion-comment <disc-node-id> <file> [reply-to-node-id] Create discussion comment (GraphQL)",
" notify <target> <author> <type> <types> [reply-to-node-id|body-result-file] Post notification",
" resolve <n> [resolution] [comment] Close alert",
" list-open List open alerts",
" summary <json-file> Print formatted summary",
].join("\n"),
);
process.exit(1);
}
commands[command]();
}
commands[command]();
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
main();
}

View File

@@ -24,8 +24,11 @@ Prove the touched surface first. Do not reflexively run the whole suite.
- normal source checkout, one failing file: `pnpm test <path-or-filter> -- --reporter=verbose`
- Codex worktree or linked/sparse checkout, one/few explicit files: `node scripts/run-vitest.mjs <path-or-filter>`
- Codex worktree or linked/sparse checkout, changed gates or anything broad:
`node scripts/crabbox-wrapper.mjs run ... --shell -- "pnpm check:changed"`
and let `.crabbox.yaml` choose the provider
use the Crabbox wrapper with the provider that matches the proof surface.
For maintainer heavy `pnpm` gates, that is usually delegated Blacksmith
Testbox through Crabbox, e.g. `node scripts/crabbox-wrapper.mjs run
--provider blacksmith-testbox ... -- pnpm check:changed`. For direct AWS
Crabbox proof, omit `--provider` and let `.crabbox.yaml` choose AWS.
- workflow-only: `git diff --check`, workflow syntax/lint (`actionlint` when available)
- docs-only: `pnpm docs:list`, docs formatter/lint only if docs tooling changed or requested
2. Reproduce narrowly before fixing.
@@ -46,13 +49,18 @@ Prove the touched surface first. Do not reflexively run the whole suite.
`node scripts/run-vitest.mjs` for tiny local proof, `node
scripts/crabbox-wrapper.mjs` for Testbox, and `git commit --no-verify` only
after the relevant remote or node-wrapper proof is already clean.
- For remote proof, use Crabbox first and omit `--provider` unless a specific
provider is being tested. The repo Crabbox config routes normal broad proof to
brokered AWS. Blacksmith Testbox is explicit opt-in; if it queues, fails
capacity, or cannot allocate, retry once through the default Crabbox route or
report the Testbox blocker. Reuse only an id/slug created in this operator
session; `blacksmith testbox list` is diagnostics only, not a shared work
queue.
- For remote proof, use the Crabbox wrapper first, but name the actual backend.
Direct AWS Crabbox uses `provider=aws` and `cbx_...` ids. Delegated
Blacksmith Testbox through Crabbox uses `provider=blacksmith-testbox`,
`syncDelegated=true`, and `tbx_...` ids. Both satisfy "remote proof" when the
requested proof surface allows either.
- Do not infer "no Testbox is running" from plain `blacksmith testbox list`.
Use `blacksmith testbox list --all` or `blacksmith testbox status <tbx_id>`
before reporting cloud state.
- Reuse only an id/slug created in this operator session unless explicitly
coordinating with another lane. If Testbox queues, fails capacity, or cannot
allocate, report the blocker or switch to direct AWS Crabbox only when that
still proves the requested surface.
## Local Test Shortcuts

View File

@@ -0,0 +1,41 @@
---
name: slacrawl
description: "Slack archive: search, sync freshness, threads/DMs, SQL counts, and Slacrawl repo work."
metadata:
openclaw:
homepage: https://github.com/openclaw/slacrawl
requires:
bins:
- slacrawl
install:
- kind: go
module: github.com/vincentkoc/slacrawl/cmd/slacrawl@latest
bins:
- slacrawl
---
# Slacrawl
Use local Slack archive data first. Check freshness for recent/current questions:
```bash
slacrawl doctor
slacrawl status --json
```
Refresh only when stale or asked:
```bash
slacrawl sync --source desktop
slacrawl sync --source api --latest-only
```
Query with bounded slices:
```bash
slacrawl search --limit 20 "query"
slacrawl messages --since 7d --limit 50
slacrawl sql "select count(*) from messages;"
```
Report workspace/channel names, absolute date spans, counts, and token/source limits. Use read-only SQL for exact counts/rankings. API sync and full thread/DM hydration require Slack tokens; do not assume they exist.

View File

@@ -0,0 +1,4 @@
interface:
display_name: "Slacrawl"
short_description: "Search local Slack archives and freshness"
default_prompt: "Use $slacrawl to search local Slack archives, check freshness, inspect channel or DM slices, and report exact date spans and token/source limits."

View File

@@ -16,8 +16,11 @@ Hard limits:
- Do not finish with tiny, cropped-wrong, off-bottom, or sidebar-heavy GIFs.
- Do not invent a generic proof. The proof must match the PR behavior.
- Do not force GIFs for internal-only, workflow-only, test-only, docs-only, or
otherwise non-visual PRs. A no-visual-proof manifest is a successful outcome
when GIFs would be misleading.
otherwise non-visual PRs. A no-visual-proof manifest is a successful workflow
outcome when GIFs would be misleading, but it is not proof that the PR passed.
- Keep public-facing manifest summaries short and user-domain. Do not mention
harness internals, mock-provider limits, secret/trust boundaries, local paths,
transcript seeding, or workflow implementation details in the summary.
Inputs are provided as environment variables:
@@ -42,9 +45,10 @@ Required workflow:
before/after. If it does not, write
`${MANTIS_OUTPUT_DIR}/mantis-evidence.json` with `comparison.pass: true`, no
artifacts, and a summary that starts with
`Mantis did not generate before/after GIFs because`. Include the concrete
reason in the summary. Use this manifest shape and do not create worktrees
or start Crabbox for this case:
`Mantis did not generate before/after GIFs because`. Include a short
public reason, such as `the PR changes internal session bookkeeping rather
than Telegram-visible behavior`. Use this manifest shape and do not create
worktrees or start Crabbox for this case:
```json
{
@@ -73,6 +77,14 @@ Required workflow:
}
```
If the PR appears visual but proof is blocked by Telegram Desktop session
state, authorization, credentials, Crabbox, or another capture-infrastructure
issue, do not describe it as a no-visual PR. Write a manifest with
`comparison.pass: false`, skipped lanes, no artifacts, and a summary that
starts with `Mantis could not capture Telegram Desktop proof because`. The
publisher will keep that out of PR comments so the failure stays in the
workflow logs and artifacts.
4. Decide what Telegram message, mock model response, command, callback, button,
media, or sequence best proves the PR. Use `MANTIS_INSTRUCTIONS` as extra
maintainer guidance, not as a replacement for reading the PR.
@@ -134,4 +146,6 @@ Expected final state:
`Main` and `This PR`.
- No-visual-proof manifests contain no artifacts and have `comparison.pass:
true`.
- Capture-infrastructure failure manifests contain no artifacts and have
`comparison.pass: false`.
- The worktree can be dirty only under `.artifacts/`.

2
.github/labeler.yml vendored
View File

@@ -101,7 +101,9 @@
- changed-files:
- any-glob-to-any-file:
- "extensions/qa-lab/**"
- "qa/scenarios/**"
- "docs/concepts/qa-e2e-automation.md"
- "docs/concepts/personal-agent-benchmark-pack.md"
- "docs/channels/qa-channel.md"
"channel: signal":
- changed-files:

View File

@@ -20,6 +20,8 @@ on:
- "docs/**"
pull_request:
types: [opened, reopened, synchronize, ready_for_review, converted_to_draft]
paths-ignore:
- "CHANGELOG.md"
permissions:
contents: read
@@ -38,7 +40,7 @@ jobs:
permissions:
contents: read
if: github.event_name != 'pull_request' || !github.event.pull_request.draft
runs-on: ubuntu-24.04
runs-on: ${{ github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-24.04') }}
timeout-minutes: 20
outputs:
checkout_revision: ${{ steps.checkout_ref.outputs.sha }}
@@ -301,7 +303,7 @@ jobs:
permissions:
contents: read
if: github.event_name != 'pull_request' || !github.event.pull_request.draft
runs-on: ubuntu-24.04
runs-on: ${{ github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-24.04') }}
timeout-minutes: 20
env:
PRE_COMMIT_HOME: .cache/pre-commit-security-fast
@@ -394,7 +396,7 @@ jobs:
permissions:
contents: read
if: github.event_name != 'pull_request' || !github.event.pull_request.draft
runs-on: ubuntu-24.04
runs-on: ${{ github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-24.04') }}
timeout-minutes: 10
steps:
- name: Checkout
@@ -419,7 +421,7 @@ jobs:
permissions: {}
needs: [security-scm-fast, security-dependency-audit]
if: ${{ !cancelled() && always() && (github.event_name != 'pull_request' || !github.event.pull_request.draft) }}
runs-on: ubuntu-24.04
runs-on: ${{ github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-24.04') }}
timeout-minutes: 5
steps:
- name: Verify fast security jobs
@@ -641,6 +643,15 @@ jobs:
echo "${name}-result=${results[$name]}" >> "$GITHUB_OUTPUT"
done
failures=0
for name in channels core-support-boundary gateway-watch; do
if [ "${results[$name]}" = "failure" ]; then
echo "::error title=${name} failed::${name} failed"
failures=1
fi
done
exit "$failures"
- name: Upload gateway watch regression artifacts
if: always() && needs.preflight.outputs.run_check_additional == 'true'
uses: actions/upload-artifact@v7
@@ -828,28 +839,6 @@ jobs:
EOF
OPENCLAW_VITEST_INCLUDE_FILE="$include_file" pnpm test:contracts:plugins
checks-fast-plugin-contracts:
permissions:
contents: read
name: checks-fast-contracts-plugins
needs: [preflight, checks-fast-plugin-contracts-shard]
if: ${{ !cancelled() && always() && needs.preflight.outputs.run_plugin_contracts_shards == 'true' }}
runs-on: ubuntu-24.04
timeout-minutes: 5
steps:
- name: Verify plugin contract shards
env:
SHARD_RESULT: ${{ needs.checks-fast-plugin-contracts-shard.result }}
run: |
if [ "$SHARD_RESULT" = "cancelled" ]; then
echo "Plugin contract shards were cancelled, usually because a newer commit superseded this run." >&2
exit 1
fi
if [ "$SHARD_RESULT" != "success" ]; then
echo "Plugin contract shards failed: $SHARD_RESULT" >&2
exit 1
fi
checks-fast-channel-contracts-shard:
permissions:
contents: read
@@ -934,35 +923,13 @@ jobs:
EOF
OPENCLAW_VITEST_INCLUDE_FILE="$include_file" pnpm test:contracts:channels
checks-fast-channel-contracts:
permissions:
contents: read
name: checks-fast-contracts-channels
needs: [preflight, checks-fast-channel-contracts-shard]
if: ${{ !cancelled() && always() && needs.preflight.outputs.run_checks_fast == 'true' }}
runs-on: ubuntu-24.04
timeout-minutes: 5
steps:
- name: Verify channel contract shards
env:
SHARD_RESULT: ${{ needs.checks-fast-channel-contracts-shard.result }}
run: |
if [ "$SHARD_RESULT" = "cancelled" ]; then
echo "Channel contract shards were cancelled, usually because a newer commit superseded this run." >&2
exit 1
fi
if [ "$SHARD_RESULT" != "success" ]; then
echo "Channel contract shards failed: $SHARD_RESULT" >&2
exit 1
fi
checks-fast-protocol:
permissions:
contents: read
name: "checks-fast-protocol"
needs: [preflight]
if: needs.preflight.outputs.run_checks_fast == 'true'
runs-on: ubuntu-24.04
runs-on: ${{ github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-24.04') }}
timeout-minutes: 30
steps:
- name: Checkout
@@ -1021,38 +988,6 @@ jobs:
- name: Run protocol check
run: pnpm protocol:check
checks:
permissions:
contents: read
name: ${{ matrix.check_name }}
needs: [preflight, build-artifacts]
if: ${{ !cancelled() && always() && needs.preflight.outputs.run_checks == 'true' && needs.build-artifacts.result == 'success' }}
runs-on: ubuntu-24.04
timeout-minutes: 5
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.preflight.outputs.checks_matrix) }}
steps:
- name: Verify ${{ matrix.task }} (${{ matrix.runtime }})
env:
TASK: ${{ matrix.task }}
CHANNELS_RESULT: ${{ needs.build-artifacts.outputs['channels-result'] }}
shell: bash
run: |
set -euo pipefail
case "$TASK" in
channels)
if [ "$CHANNELS_RESULT" != "success" ]; then
echo "Channel tests failed in build-artifacts: $CHANNELS_RESULT" >&2
exit 1
fi
;;
*)
echo "Unsupported checks task: $TASK" >&2
exit 1
;;
esac
checks-node-compat:
permissions:
contents: read
@@ -1240,63 +1175,6 @@ jobs:
}
EOF
checks-node-core-test-dist-shard:
permissions:
contents: read
name: ${{ matrix.check_name }}
needs: [preflight, build-artifacts]
if: ${{ !cancelled() && always() && needs.preflight.outputs.run_checks_node_core_dist == 'true' && needs.build-artifacts.result == 'success' }}
runs-on: ubuntu-24.04
timeout-minutes: 5
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.preflight.outputs.checks_node_core_dist_matrix) }}
steps:
- name: Verify Node test shard
env:
CORE_SUPPORT_BOUNDARY_RESULT: ${{ needs.build-artifacts.outputs['core-support-boundary-result'] }}
SHARD_NAME: ${{ matrix.shard_name }}
shell: bash
run: |
set -euo pipefail
case "$SHARD_NAME" in
core-support-boundary)
if [ "$CORE_SUPPORT_BOUNDARY_RESULT" != "success" ]; then
echo "Core support boundary shard failed in build-artifacts: $CORE_SUPPORT_BOUNDARY_RESULT" >&2
exit 1
fi
;;
*)
echo "Unsupported built-artifact shard: $SHARD_NAME" >&2
exit 1
;;
esac
checks-node-core-test:
permissions:
contents: read
name: checks-node-core
needs: [preflight, checks-node-core-test-nondist-shard, checks-node-core-test-dist-shard]
if: ${{ !cancelled() && always() && needs.preflight.outputs.run_checks == 'true' }}
runs-on: ubuntu-24.04
timeout-minutes: 5
steps:
- name: Verify node test shards
env:
DIST_SHARD_RESULT: ${{ needs.checks-node-core-test-dist-shard.result }}
NONDIST_SHARD_RESULT: ${{ needs.checks-node-core-test-nondist-shard.result }}
RUN_DIST_SHARDS: ${{ needs.preflight.outputs.run_checks_node_core_dist }}
RUN_NONDIST_SHARDS: ${{ needs.preflight.outputs.run_checks_node_core_nondist }}
run: |
if [ "$RUN_NONDIST_SHARDS" = "true" ] && [ "$NONDIST_SHARD_RESULT" != "success" ]; then
echo "Node non-dist test shards failed: $NONDIST_SHARD_RESULT" >&2
exit 1
fi
if [ "$RUN_DIST_SHARDS" = "true" ] && [ "$DIST_SHARD_RESULT" != "success" ]; then
echo "Node dist test shards failed: $DIST_SHARD_RESULT" >&2
exit 1
fi
# Types, lint, and format check shards.
check-shard:
permissions:
@@ -1312,7 +1190,7 @@ jobs:
include:
- check_name: check-preflight-guards
task: preflight-guards
runner: ubuntu-24.04
runner: blacksmith-4vcpu-ubuntu-2404
- check_name: check-prod-types
task: prod-types
runner: blacksmith-4vcpu-ubuntu-2404
@@ -1321,16 +1199,16 @@ jobs:
runner: blacksmith-16vcpu-ubuntu-2404
- check_name: check-dependencies
task: dependencies
runner: ubuntu-24.04
runner: blacksmith-8vcpu-ubuntu-2404
- check_name: check-policy-guards
task: policy-guards
runner: ubuntu-24.04
runner: blacksmith-4vcpu-ubuntu-2404
- check_name: check-test-types
task: test-types
runner: blacksmith-4vcpu-ubuntu-2404
- check_name: check-strict-smoke
task: strict-smoke
runner: ubuntu-24.04
runner: blacksmith-4vcpu-ubuntu-2404
steps:
- name: Checkout
shell: bash
@@ -1442,24 +1320,6 @@ jobs:
path: .artifacts/deadcode
if-no-files-found: ignore
check:
permissions:
contents: read
name: "check"
needs: [preflight, check-shard]
if: ${{ !cancelled() && always() && needs.preflight.outputs.run_check == 'true' }}
runs-on: ubuntu-24.04
timeout-minutes: 5
steps:
- name: Verify check shards
env:
SHARD_RESULT: ${{ needs.check-shard.result }}
run: |
if [ "$SHARD_RESULT" != "success" ]; then
echo "Check shards failed: $SHARD_RESULT" >&2
exit 1
fi
check-additional-shard:
permissions:
contents: read
@@ -1637,59 +1497,13 @@ jobs:
exit "$failures"
check-additional:
permissions:
contents: read
name: "check-additional"
needs: [preflight, check-additional-shard, build-artifacts]
if: ${{ !cancelled() && always() && needs.preflight.outputs.run_check_additional == 'true' }}
runs-on: ubuntu-24.04
timeout-minutes: 5
steps:
- name: Verify additional check shards
env:
SHARD_RESULT: ${{ needs.check-additional-shard.result }}
BUILD_ARTIFACTS_RESULT: ${{ needs.build-artifacts.result }}
GATEWAY_RESULT: ${{ needs.build-artifacts.outputs.gateway-watch-result }}
run: |
if [ "$SHARD_RESULT" != "success" ]; then
echo "Additional check shards failed: $SHARD_RESULT" >&2
exit 1
fi
if [ "$BUILD_ARTIFACTS_RESULT" != "success" ]; then
echo "Build artifact job failed: $BUILD_ARTIFACTS_RESULT" >&2
exit 1
fi
if [ "$GATEWAY_RESULT" != "success" ]; then
echo "Gateway topology check failed: $GATEWAY_RESULT" >&2
exit 1
fi
build-smoke:
permissions:
contents: read
name: "build-smoke"
needs: [preflight, build-artifacts]
if: ${{ !cancelled() && always() && needs.preflight.outputs.run_build_smoke == 'true' && (github.event_name != 'push' || needs.build-artifacts.result == 'success') }}
runs-on: ubuntu-24.04
timeout-minutes: 5
steps:
- name: Verify build smoke
env:
BUILD_ARTIFACTS_RESULT: ${{ needs.build-artifacts.result }}
run: |
if [ "$BUILD_ARTIFACTS_RESULT" != "success" ]; then
echo "Build smoke checks failed in build-artifacts: $BUILD_ARTIFACTS_RESULT" >&2
exit 1
fi
# Validate docs (format, lint, broken links) only when docs files changed.
check-docs:
permissions:
contents: read
needs: [preflight]
if: needs.preflight.outputs.run_check_docs == 'true'
runs-on: ubuntu-24.04
runs-on: ${{ github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-24.04') }}
timeout-minutes: 20
steps:
- name: Checkout
@@ -1763,7 +1577,7 @@ jobs:
contents: read
needs: [preflight]
if: needs.preflight.outputs.run_skills_python_job == 'true'
runs-on: ubuntu-24.04
runs-on: ${{ github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-24.04') }}
timeout-minutes: 20
steps:
- name: Checkout

View File

@@ -138,7 +138,7 @@ jobs:
OPENAI_API_KEY: ${{ secrets.OPENCLAW_DOCS_I18N_OPENAI_API_KEY || secrets.OPENAI_API_KEY }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
OPENCLAW_CONTROL_UI_I18N_PROVIDER: ${{ secrets.ANTHROPIC_API_KEY != '' && 'anthropic' || 'openai' }}
OPENCLAW_CONTROL_UI_I18N_MODEL: ${{ secrets.ANTHROPIC_API_KEY != '' && 'claude-opus-4-6' || vars.OPENCLAW_CI_OPENAI_MODEL_BARE }}
OPENCLAW_CONTROL_UI_I18N_MODEL: ${{ secrets.ANTHROPIC_API_KEY != '' && 'claude-opus-4-7' || vars.OPENCLAW_CI_OPENAI_MODEL_BARE }}
OPENCLAW_CONTROL_UI_I18N_THINKING: low
OPENCLAW_CONTROL_UI_I18N_AUTH_OPTIONAL: "1"
LOCALE: ${{ matrix.locale }}

View File

@@ -6,6 +6,7 @@ on:
paths:
- "**/*.md"
- "docs/**"
- "!CHANGELOG.md"
permissions:
contents: read

View File

@@ -638,6 +638,7 @@ jobs:
name: Run package Telegram E2E
needs: [resolve_target, prepare_release_package]
if: ${{ always() && contains(fromJSON('["all","npm-telegram"]'), inputs.rerun_group) && (inputs.npm_telegram_package_spec != '' || inputs.release_package_spec != '' || (inputs.rerun_group == 'all' && inputs.release_profile == 'full')) }}
continue-on-error: ${{ startsWith(github.ref, 'refs/heads/tideclaw/alpha/') }}
runs-on: ubuntu-24.04
timeout-minutes: ${{ inputs.release_profile == 'full' && 120 || 60 }}
outputs:
@@ -955,6 +956,8 @@ jobs:
if [[ "$NPM_TELEGRAM_RESULT" == "skipped" && -z "${NPM_TELEGRAM_RUN_ID// }" ]]; then
check_child "npm_telegram" "" 0 || failed=1
elif [[ "$CHILD_WORKFLOW_REF" =~ ^tideclaw/alpha/[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{4}Z$ ]]; then
check_child "npm_telegram" "$NPM_TELEGRAM_RUN_ID" 0 || echo "::warning::npm_telegram is advisory for Tideclaw alpha validation."
else
check_child "npm_telegram" "$NPM_TELEGRAM_RUN_ID" 1 || failed=1
fi

View File

@@ -33,8 +33,11 @@ jobs:
authorize_actor:
name: Authorize workflow actor
runs-on: blacksmith-8vcpu-ubuntu-2404
outputs:
authorized: ${{ steps.permission.outputs.authorized }}
steps:
- name: Require maintainer-level repository access
id: permission
uses: actions/github-script@v8
with:
script: |
@@ -48,14 +51,18 @@ jobs:
const permission = data.permission;
core.info(`Actor ${context.actor} permission: ${permission}`);
if (!allowed.has(permission)) {
core.setFailed(
core.notice(
`Workflow requires write/maintain/admin access. Actor "${context.actor}" has "${permission}".`,
);
core.setOutput("authorized", "false");
return;
}
core.setOutput("authorized", "true");
validate_selected_ref:
name: Validate selected ref
needs: authorize_actor
if: needs.authorize_actor.outputs.authorized == 'true'
runs-on: blacksmith-8vcpu-ubuntu-2404
outputs:
selected_revision: ${{ steps.validate.outputs.selected_revision }}

View File

@@ -46,15 +46,17 @@ jobs:
github.event_name == 'issue_comment' &&
github.event.issue.pull_request &&
(
contains(github.event.comment.body, '@Mantis') ||
contains(github.event.comment.body, '@mantis') ||
contains(github.event.comment.body, '/mantis')
contains(github.event.comment.body, '@openclaw-mantis') ||
contains(github.event.comment.body, '/openclaw-mantis')
)
)
}}
runs-on: blacksmith-8vcpu-ubuntu-2404
outputs:
authorized: ${{ steps.permission.outputs.authorized }}
steps:
- name: Require maintainer-level repository access
id: permission
uses: actions/github-script@v8
with:
script: |
@@ -68,14 +70,18 @@ jobs:
const permission = data.permission;
core.info(`Actor ${context.actor} permission: ${permission}`);
if (!allowed.has(permission)) {
core.setFailed(
core.notice(
`Workflow requires write/maintain/admin access. Actor "${context.actor}" has "${permission}".`,
);
core.setOutput("authorized", "false");
return;
}
core.setOutput("authorized", "true");
resolve_request:
name: Resolve Mantis request
needs: authorize_actor
if: needs.authorize_actor.outputs.authorized == 'true'
runs-on: blacksmith-8vcpu-ubuntu-2404
outputs:
baseline_ref: ${{ steps.resolve.outputs.baseline_ref }}
@@ -121,7 +127,7 @@ jobs:
const normalized = body.toLowerCase();
const requested =
(normalized.includes("@mantis") || normalized.includes("/mantis")) &&
(normalized.includes("@openclaw-mantis") || normalized.includes("/openclaw-mantis")) &&
normalized.includes("discord") &&
normalized.includes("status") &&
normalized.includes("reaction");
@@ -342,8 +348,8 @@ jobs:
--repo-root "$repo_root" \
--output-dir "$output_dir" \
--provider-mode live-frontier \
--model openai/gpt-5.4 \
--alt-model openai/gpt-5.4 \
--model openai/gpt-5.5 \
--alt-model openai/gpt-5.5 \
--fast \
--credential-source convex \
--credential-role ci \
@@ -567,3 +573,44 @@ jobs:
--artifact-url "$ARTIFACT_URL" \
--run-url "https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" \
--request-source "$REQUEST_SOURCE"
clear_issue_comment_reaction:
name: Clear Mantis command reaction
needs: [resolve_request, validate_refs, run_status_reactions]
if: ${{ always() && github.event_name == 'issue_comment' && needs.resolve_request.outputs.request_source == 'issue_comment' }}
runs-on: ubuntu-24.04
permissions:
issues: write
steps:
- name: Remove workflow eyes reaction
uses: actions/github-script@v8
with:
script: |
const { owner, repo } = context.repo;
const commentId = context.payload.comment?.id;
if (!commentId) {
core.info("No issue comment id found; skipping reaction cleanup.");
return;
}
const reactions = await github.paginate(github.rest.reactions.listForIssueComment, {
owner,
repo,
comment_id: commentId,
per_page: 100,
});
const eyes = reactions.filter(
(reaction) => reaction.content === "eyes" && reaction.user?.login === "github-actions[bot]",
);
for (const reaction of eyes) {
await github.rest.reactions.deleteForIssueComment({
owner,
repo,
comment_id: commentId,
reaction_id: reaction.id,
});
core.info(`Removed eyes reaction ${reaction.id} from comment ${commentId}.`);
}
if (eyes.length === 0) {
core.info(`No workflow eyes reaction found on comment ${commentId}.`);
}

View File

@@ -46,15 +46,17 @@ jobs:
github.event_name == 'issue_comment' &&
github.event.issue.pull_request &&
(
contains(github.event.comment.body, '@Mantis') ||
contains(github.event.comment.body, '@mantis') ||
contains(github.event.comment.body, '/mantis')
contains(github.event.comment.body, '@openclaw-mantis') ||
contains(github.event.comment.body, '/openclaw-mantis')
)
)
}}
runs-on: blacksmith-8vcpu-ubuntu-2404
outputs:
authorized: ${{ steps.permission.outputs.authorized }}
steps:
- name: Require maintainer-level repository access
id: permission
uses: actions/github-script@v8
with:
script: |
@@ -68,14 +70,18 @@ jobs:
const permission = data.permission;
core.info(`Actor ${context.actor} permission: ${permission}`);
if (!allowed.has(permission)) {
core.setFailed(
core.notice(
`Workflow requires write/maintain/admin access. Actor "${context.actor}" has "${permission}".`,
);
core.setOutput("authorized", "false");
return;
}
core.setOutput("authorized", "true");
resolve_request:
name: Resolve Mantis request
needs: authorize_actor
if: needs.authorize_actor.outputs.authorized == 'true'
runs-on: blacksmith-8vcpu-ubuntu-2404
outputs:
baseline_ref: ${{ steps.resolve.outputs.baseline_ref }}
@@ -121,7 +127,7 @@ jobs:
const normalized = body.toLowerCase();
const requested =
(normalized.includes("@mantis") || normalized.includes("/mantis")) &&
(normalized.includes("@openclaw-mantis") || normalized.includes("/openclaw-mantis")) &&
normalized.includes("discord") &&
normalized.includes("thread") &&
(normalized.includes("attachment") ||
@@ -589,3 +595,44 @@ jobs:
run: |
echo "Mantis comparison failed." >&2
exit 1
clear_issue_comment_reaction:
name: Clear Mantis command reaction
needs: [resolve_request, validate_candidate, run_thread_attachment]
if: ${{ always() && github.event_name == 'issue_comment' && needs.resolve_request.outputs.request_source == 'issue_comment' }}
runs-on: ubuntu-24.04
permissions:
issues: write
steps:
- name: Remove workflow eyes reaction
uses: actions/github-script@v8
with:
script: |
const { owner, repo } = context.repo;
const commentId = context.payload.comment?.id;
if (!commentId) {
core.info("No issue comment id found; skipping reaction cleanup.");
return;
}
const reactions = await github.paginate(github.rest.reactions.listForIssueComment, {
owner,
repo,
comment_id: commentId,
per_page: 100,
});
const eyes = reactions.filter(
(reaction) => reaction.content === "eyes" && reaction.user?.login === "github-actions[bot]",
);
for (const reaction of eyes) {
await github.rest.reactions.deleteForIssueComment({
owner,
repo,
comment_id: commentId,
reaction_id: reaction.id,
});
core.info(`Removed eyes reaction ${reaction.id} from comment ${commentId}.`);
}
if (eyes.length === 0) {
core.info(`No workflow eyes reaction found on comment ${commentId}.`);
}

View File

@@ -64,8 +64,11 @@ jobs:
authorize_actor:
name: Authorize workflow actor
runs-on: ubuntu-24.04
outputs:
authorized: ${{ steps.permission.outputs.authorized }}
steps:
- name: Require maintainer-level repository access
id: permission
uses: actions/github-script@v8
with:
script: |
@@ -79,14 +82,18 @@ jobs:
const permission = data.permission;
core.info(`Actor ${context.actor} permission: ${permission}`);
if (!allowed.has(permission)) {
core.setFailed(
core.notice(
`Workflow requires write/maintain/admin access. Actor "${context.actor}" has "${permission}".`,
);
core.setOutput("authorized", "false");
return;
}
core.setOutput("authorized", "true");
validate_ref:
name: Validate candidate ref
needs: authorize_actor
if: needs.authorize_actor.outputs.authorized == 'true'
runs-on: ubuntu-24.04
outputs:
candidate_revision: ${{ steps.validate.outputs.candidate_revision }}
@@ -274,8 +281,8 @@ jobs:
--credential-role ci \
--provider-mode live-frontier \
--hydrate-mode "$HYDRATE_MODE" \
--model openai/gpt-5.4 \
--alt-model openai/gpt-5.4 \
--model openai/gpt-5.5 \
--alt-model openai/gpt-5.5 \
--fast \
--scenario "$SCENARIO_ID" \
"${keep_args[@]}" \

View File

@@ -3,6 +3,8 @@ name: Mantis Telegram Desktop Proof
on:
issue_comment:
types: [created]
pull_request_target: # zizmor: ignore[dangerous-triggers] maintainer-owned Mantis label trigger; trusted base workflow validates refs before checkout/use
types: [labeled]
workflow_dispatch:
inputs:
pr_number:
@@ -25,6 +27,14 @@ on:
description: Optional existing Crabbox desktop lease id or slug to reuse
required: false
type: string
publish_artifact_name:
description: Optional existing proof artifact name to publish without recapturing
required: false
type: string
publish_run_id:
description: Workflow run id that owns publish_artifact_name; required with publish_artifact_name
required: false
type: string
permissions:
actions: read
@@ -47,6 +57,11 @@ jobs:
if: >-
${{
github.event_name == 'workflow_dispatch' ||
(
github.event_name == 'pull_request_target' &&
github.event.action == 'labeled' &&
github.event.label.name == 'mantis: telegram-visible-proof'
) ||
(
github.event_name == 'issue_comment' &&
github.event.issue.pull_request &&
@@ -58,11 +73,20 @@ jobs:
)
}}
runs-on: ubuntu-24.04
outputs:
authorized: ${{ steps.permission.outputs.authorized }}
steps:
- name: Require maintainer-level repository access
id: permission
uses: actions/github-script@v8
with:
script: |
if (context.eventName === "pull_request_target") {
core.info(`Accepted Mantis label trigger from ${context.actor}.`);
core.setOutput("authorized", "true");
return;
}
const allowed = new Set(["admin", "maintain", "write"]);
const { owner, repo } = context.repo;
const { data } = await github.rest.repos.getCollaboratorPermissionLevel({
@@ -73,14 +97,18 @@ jobs:
const permission = data.permission;
core.info(`Actor ${context.actor} permission: ${permission}`);
if (!allowed.has(permission)) {
core.setFailed(
core.notice(
`Workflow requires write/maintain/admin access. Actor "${context.actor}" has "${permission}".`,
);
core.setOutput("authorized", "false");
return;
}
core.setOutput("authorized", "true");
resolve_request:
name: Resolve Mantis request
needs: authorize_actor
if: needs.authorize_actor.outputs.authorized == 'true'
runs-on: ubuntu-24.04
outputs:
baseline_ref: ${{ steps.resolve.outputs.baseline_ref }}
@@ -88,8 +116,11 @@ jobs:
crabbox_provider: ${{ steps.resolve.outputs.crabbox_provider }}
instructions: ${{ steps.resolve.outputs.instructions }}
lease_id: ${{ steps.resolve.outputs.lease_id }}
publish_artifact_name: ${{ steps.resolve.outputs.publish_artifact_name }}
publish_run_id: ${{ steps.resolve.outputs.publish_run_id }}
pr_number: ${{ steps.resolve.outputs.pr_number }}
request_source: ${{ steps.resolve.outputs.request_source }}
should_run: ${{ steps.resolve.outputs.should_run }}
steps:
- name: Resolve refs and target PR
id: resolve
@@ -105,31 +136,70 @@ jobs:
const inputs = context.payload.inputs ?? {};
const prNumber =
eventName === "workflow_dispatch" ? inputs.pr_number : String(context.payload.issue?.number ?? "");
eventName === "workflow_dispatch"
? inputs.pr_number
: eventName === "pull_request_target"
? String(context.payload.pull_request?.number ?? "")
: String(context.payload.issue?.number ?? "");
if (!prNumber) {
core.setFailed("Mantis Telegram desktop proof requires a pull request.");
return;
}
const body =
eventName === "workflow_dispatch"
? inputs.instructions || ""
: eventName === "issue_comment"
? context.payload.comment?.body || ""
: "";
if (eventName === "issue_comment") {
const normalized = body.toLowerCase();
const requestedDesktopProof =
(normalized.includes("@openclaw-mantis") || normalized.includes("/openclaw-mantis")) &&
(normalized.includes("desktop proof") ||
normalized.includes("desktop-proof") ||
normalized.includes("telegram desktop") ||
normalized.includes("native telegram") ||
normalized.includes("visible proof") ||
normalized.includes("visible-proof") ||
normalized.includes("telegram-visible-proof"));
if (!requestedDesktopProof) {
core.notice("Comment mentioned Mantis but did not request Telegram desktop proof.");
setOutput("should_run", "false");
setOutput("baseline_ref", "");
setOutput("candidate_ref", "");
setOutput("pr_number", "");
setOutput("instructions", "");
setOutput("crabbox_provider", "");
setOutput("lease_id", "");
setOutput("publish_artifact_name", "");
setOutput("publish_run_id", "");
setOutput("request_source", "unsupported_issue_comment");
return;
}
}
const { owner, repo } = context.repo;
const { data: pr } = await github.rest.pulls.get({
owner,
repo,
pull_number: Number(prNumber),
});
const body = eventName === "workflow_dispatch" ? inputs.instructions || "" : context.payload.comment?.body || "";
const provider = inputs.crabbox_provider || "aws";
if (!["aws", "hetzner"].includes(provider)) {
core.setFailed(`Unsupported Crabbox provider for Mantis Telegram desktop proof: ${provider}`);
return;
}
setOutput("should_run", "true");
setOutput("baseline_ref", pr.base.sha);
setOutput("candidate_ref", pr.head.sha);
setOutput("pr_number", String(pr.number));
setOutput("instructions", body);
setOutput("crabbox_provider", provider);
setOutput("lease_id", inputs.crabbox_lease_id || "");
setOutput("publish_artifact_name", inputs.publish_artifact_name || "");
setOutput("publish_run_id", inputs.publish_run_id || "");
setOutput("request_source", eventName);
if (eventName === "issue_comment") {
@@ -144,6 +214,7 @@ jobs:
validate_refs:
name: Validate selected refs
needs: resolve_request
if: needs.resolve_request.outputs.should_run == 'true' && needs.resolve_request.outputs.publish_artifact_name == ''
runs-on: ubuntu-24.04
outputs:
baseline_revision: ${{ steps.validate.outputs.baseline_revision }}
@@ -222,6 +293,7 @@ jobs:
run_telegram_desktop_proof:
name: Run agentic native Telegram proof
needs: [resolve_request, validate_refs]
if: needs.resolve_request.outputs.should_run == 'true' && needs.resolve_request.outputs.publish_artifact_name == ''
runs-on: blacksmith-16vcpu-ubuntu-2404
timeout-minutes: 360
environment: qa-live-shared
@@ -386,6 +458,7 @@ jobs:
codex-home: /tmp/mantis-codex-home-${{ github.run_id }}
safety-strategy: unprivileged-user
codex-user: codex
allow-bot-users: clawsweeper[bot]
- name: Inspect Mantis evidence manifest
id: inspect
@@ -466,3 +539,133 @@ jobs:
run: |
echo "Mantis Telegram desktop proof failed: comparison=${COMPARISON_STATUS:-unset}." >&2
exit 1
publish_existing_telegram_desktop_proof:
name: Publish existing native Telegram proof
needs: resolve_request
if: needs.resolve_request.outputs.should_run == 'true' && needs.resolve_request.outputs.publish_artifact_name != ''
runs-on: ubuntu-24.04
environment: qa-live-shared
steps:
- name: Checkout harness ref
uses: actions/checkout@v6
with:
persist-credentials: false
- 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: Download existing proof artifact
env:
GH_TOKEN: ${{ github.token }}
PUBLISH_ARTIFACT_NAME: ${{ needs.resolve_request.outputs.publish_artifact_name }}
PUBLISH_RUN_ID: ${{ needs.resolve_request.outputs.publish_run_id }}
shell: bash
run: |
set -euo pipefail
if [[ -z "${PUBLISH_RUN_ID:-}" ]]; then
echo "publish_run_id is required when publish_artifact_name is set." >&2
exit 1
fi
run_id="$PUBLISH_RUN_ID"
gh run download "$run_id" \
--repo "$GITHUB_REPOSITORY" \
--name "$PUBLISH_ARTIFACT_NAME" \
--dir "$MANTIS_OUTPUT_DIR"
artifacts_json="$(
gh api \
-H "Accept: application/vnd.github+json" \
"repos/${GITHUB_REPOSITORY}/actions/runs/${run_id}/artifacts"
)"
artifact_id="$(jq -r --arg name "$PUBLISH_ARTIFACT_NAME" '.artifacts[] | select(.name == $name) | .id' <<<"$artifacts_json" | head -n 1)"
if [[ -z "$artifact_id" || "$artifact_id" == "null" ]]; then
echo "Could not resolve artifact id for '${PUBLISH_ARTIFACT_NAME}' in run ${run_id}." >&2
exit 1
fi
echo "PUBLISH_RUN_ID=${run_id}" >> "$GITHUB_ENV"
echo "PUBLISH_ARTIFACT_URL=https://github.com/${GITHUB_REPOSITORY}/actions/runs/${run_id}/artifacts/${artifact_id}" >> "$GITHUB_ENV"
- name: Create Mantis GitHub App token
id: mantis_app_token
uses: actions/create-github-app-token@v3
with:
app-id: ${{ secrets.MANTIS_GITHUB_APP_ID }}
private-key: ${{ secrets.MANTIS_GITHUB_APP_PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
repositories: ${{ github.event.repository.name }}
permission-issues: write
permission-pull-requests: write
- name: Comment PR with inline QA evidence
env:
GH_TOKEN: ${{ steps.mantis_app_token.outputs.token }}
MANTIS_ARTIFACT_R2_ACCESS_KEY_ID: ${{ secrets.MANTIS_ARTIFACT_R2_ACCESS_KEY_ID }}
MANTIS_ARTIFACT_R2_BUCKET: openclaw-crabbox-artifacts
MANTIS_ARTIFACT_R2_ENDPOINT: ${{ vars.MANTIS_ARTIFACT_R2_ENDPOINT }}
MANTIS_ARTIFACT_R2_PUBLIC_BASE_URL: https://artifacts.openclaw.ai
MANTIS_ARTIFACT_R2_REGION: auto
MANTIS_ARTIFACT_R2_SECRET_ACCESS_KEY: ${{ secrets.MANTIS_ARTIFACT_R2_SECRET_ACCESS_KEY }}
REQUEST_SOURCE: ${{ needs.resolve_request.outputs.request_source }}
TARGET_PR: ${{ needs.resolve_request.outputs.pr_number }}
shell: bash
run: |
set -euo pipefail
root="$MANTIS_OUTPUT_DIR"
if [[ ! -f "$root/mantis-evidence.json" ]]; then
echo "Downloaded artifact does not contain ${root}/mantis-evidence.json." >&2
exit 1
fi
node scripts/mantis/publish-pr-evidence.mjs \
--manifest "$root/mantis-evidence.json" \
--target-pr "$TARGET_PR" \
--artifact-root "mantis/telegram-desktop/pr-${TARGET_PR}/published-${PUBLISH_RUN_ID}-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" \
--marker "<!-- mantis-telegram-desktop-proof -->" \
--artifact-url "$PUBLISH_ARTIFACT_URL" \
--run-url "https://github.com/${GITHUB_REPOSITORY}/actions/runs/${PUBLISH_RUN_ID}" \
--request-source "$REQUEST_SOURCE"
clear_issue_comment_reaction:
name: Clear Mantis command reaction
needs: [resolve_request, validate_refs, run_telegram_desktop_proof]
if: ${{ always() && github.event_name == 'issue_comment' && needs.resolve_request.outputs.request_source == 'issue_comment' }}
runs-on: ubuntu-24.04
permissions:
issues: write
steps:
- name: Remove workflow eyes reaction
uses: actions/github-script@v8
with:
script: |
const { owner, repo } = context.repo;
const commentId = context.payload.comment?.id;
if (!commentId) {
core.info("No issue comment id found; skipping reaction cleanup.");
return;
}
const reactions = await github.paginate(github.rest.reactions.listForIssueComment, {
owner,
repo,
comment_id: commentId,
per_page: 100,
});
const eyes = reactions.filter(
(reaction) => reaction.content === "eyes" && reaction.user?.login === "github-actions[bot]",
);
for (const reaction of eyes) {
await github.rest.reactions.deleteForIssueComment({
owner,
repo,
comment_id: commentId,
reaction_id: reaction.id,
});
core.info(`Removed eyes reaction ${reaction.id} from comment ${commentId}.`);
}
if (eyes.length === 0) {
core.info(`No workflow eyes reaction found on comment ${commentId}.`);
}

View File

@@ -56,15 +56,17 @@ jobs:
github.event_name == 'issue_comment' &&
github.event.issue.pull_request &&
(
contains(github.event.comment.body, '@Mantis') ||
contains(github.event.comment.body, '@mantis') ||
contains(github.event.comment.body, '/mantis')
contains(github.event.comment.body, '@openclaw-mantis') ||
contains(github.event.comment.body, '/openclaw-mantis')
)
)
}}
runs-on: ubuntu-24.04
outputs:
authorized: ${{ steps.permission.outputs.authorized }}
steps:
- name: Require maintainer-level repository access
id: permission
uses: actions/github-script@v8
with:
script: |
@@ -78,14 +80,18 @@ jobs:
const permission = data.permission;
core.info(`Actor ${context.actor} permission: ${permission}`);
if (!allowed.has(permission)) {
core.setFailed(
core.notice(
`Workflow requires write/maintain/admin access. Actor "${context.actor}" has "${permission}".`,
);
core.setOutput("authorized", "false");
return;
}
core.setOutput("authorized", "true");
resolve_request:
name: Resolve Mantis request
needs: authorize_actor
if: needs.authorize_actor.outputs.authorized == 'true'
runs-on: ubuntu-24.04
outputs:
candidate_ref: ${{ steps.resolve.outputs.candidate_ref }}
@@ -133,9 +139,18 @@ jobs:
}
const normalized = body.toLowerCase();
const requestedDesktopProof =
normalized.includes("desktop proof") ||
normalized.includes("desktop-proof") ||
normalized.includes("telegram desktop") ||
normalized.includes("native telegram") ||
normalized.includes("visible proof") ||
normalized.includes("visible-proof") ||
normalized.includes("telegram-visible-proof");
const requested =
(normalized.includes("@mantis") || normalized.includes("/mantis")) &&
normalized.includes("telegram");
(normalized.includes("@openclaw-mantis") || normalized.includes("/openclaw-mantis")) &&
normalized.includes("telegram") &&
!requestedDesktopProof;
if (!requested) {
core.notice("Comment mentioned Mantis but did not request Telegram live QA.");
setOutput("should_run", "false");
@@ -379,7 +394,7 @@ jobs:
output_rel=".artifacts/qa-e2e/mantis/telegram-live"
root="$candidate_repo/$output_rel"
echo "output_dir=${root}" >> "$GITHUB_OUTPUT"
model="${OPENCLAW_CI_OPENAI_MODEL:-openai/gpt-5.4}"
model="${OPENCLAW_CI_OPENAI_MODEL:-openai/gpt-5.5}"
scenario_args=()
if [[ -n "${SCENARIO_INPUT// }" ]]; then
@@ -525,3 +540,44 @@ jobs:
run: |
echo "Mantis Telegram live failed: comparison=${COMPARISON_STATUS:-unset} telegram_exit=${TELEGRAM_EXIT:-unset}." >&2
exit 1
clear_issue_comment_reaction:
name: Clear Mantis command reaction
needs: [resolve_request, validate_ref, run_telegram_live]
if: ${{ always() && github.event_name == 'issue_comment' && needs.resolve_request.outputs.request_source == 'issue_comment' }}
runs-on: ubuntu-24.04
permissions:
issues: write
steps:
- name: Remove workflow eyes reaction
uses: actions/github-script@v8
with:
script: |
const { owner, repo } = context.repo;
const commentId = context.payload.comment?.id;
if (!commentId) {
core.info("No issue comment id found; skipping reaction cleanup.");
return;
}
const reactions = await github.paginate(github.rest.reactions.listForIssueComment, {
owner,
repo,
comment_id: commentId,
per_page: 100,
});
const eyes = reactions.filter(
(reaction) => reaction.content === "eyes" && reaction.user?.login === "github-actions[bot]",
);
for (const reaction of eyes) {
await github.rest.reactions.deleteForIssueComment({
owner,
repo,
comment_id: commentId,
reaction_id: reaction.id,
});
core.info(`Removed eyes reaction ${reaction.id} from comment ${commentId}.`);
}
if (eyes.length === 0) {
core.info(`No workflow eyes reaction found on comment ${commentId}.`);
}

View File

@@ -40,8 +40,18 @@ on:
description: Optional comma-separated Telegram scenario ids
required: false
type: string
advisory:
description: Treat package Telegram failures as advisory for the caller
required: false
default: false
type: boolean
workflow_call:
inputs:
advisory:
description: Treat package Telegram failures as advisory for the caller
required: false
default: false
type: boolean
package_spec:
description: Published OpenClaw package spec to test when no artifact is supplied
required: true
@@ -100,6 +110,7 @@ jobs:
run_package_telegram_e2e:
name: Run package Telegram E2E
runs-on: blacksmith-32vcpu-ubuntu-2404
continue-on-error: ${{ inputs.advisory }}
timeout-minutes: 60
environment: qa-live-shared
permissions:

View File

@@ -86,8 +86,18 @@ on:
required: false
default: ""
type: string
advisory:
description: Treat failures as advisory for the caller
required: false
default: false
type: boolean
workflow_call:
inputs:
advisory:
description: Treat failures as advisory for the caller
required: false
default: false
type: boolean
ref:
description: Public OpenClaw ref to validate (tag, branch, or full commit SHA)
required: true
@@ -186,11 +196,12 @@ env:
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.4' }}
OPENCLAW_CROSS_OS_OPENAI_MODEL: ${{ inputs.openai_model || vars.OPENCLAW_CROSS_OS_OPENAI_MODEL || 'openai/gpt-5.5' }}
jobs:
prepare:
runs-on: ubuntu-24.04
continue-on-error: ${{ inputs.advisory }}
outputs:
baseline_file_name: ${{ steps.baseline_metadata.outputs.file_name }}
baseline_spec: ${{ steps.baseline.outputs.value }}
@@ -513,6 +524,7 @@ jobs:
cross_os_release_checks:
name: "${{ matrix.display_name }} / ${{ matrix.suite_label }}"
needs: prepare
continue-on-error: ${{ inputs.advisory }}
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.prepare.outputs.matrix) }}

View File

@@ -97,8 +97,18 @@ on:
- beta
- stable
- full
advisory:
description: Treat failures as advisory for the caller
required: false
default: false
type: boolean
workflow_call:
inputs:
advisory:
description: Treat failures as advisory for the caller
required: false
default: false
type: boolean
ref:
description: Ref, tag, or SHA to validate
required: true
@@ -455,6 +465,7 @@ jobs:
validate_release_live_cache:
needs: validate_selected_ref
if: inputs.include_live_suites && !inputs.live_models_only && (inputs.live_suite_filter == '' || inputs.live_suite_filter == 'live-cache')
continue-on-error: ${{ inputs.advisory }}
runs-on: ${{ github.event_name == 'workflow_call' && 'ubuntu-24.04' || 'blacksmith-8vcpu-ubuntu-2404' }}
timeout-minutes: 20
env:
@@ -505,6 +516,7 @@ jobs:
validate_repo_e2e:
needs: validate_selected_ref
if: inputs.include_repo_e2e && inputs.live_suite_filter == ''
continue-on-error: ${{ inputs.advisory }}
runs-on: ${{ github.event_name == 'workflow_call' && 'ubuntu-24.04' || 'blacksmith-8vcpu-ubuntu-2404' }}
timeout-minutes: ${{ inputs.release_test_profile == 'full' && 90 || 60 }}
env:
@@ -534,6 +546,7 @@ jobs:
validate_special_e2e:
needs: validate_selected_ref
if: inputs.include_repo_e2e && (inputs.live_suite_filter == '' || inputs.live_suite_filter == 'openshell-e2e')
continue-on-error: ${{ inputs.advisory }}
runs-on: ${{ github.event_name == 'workflow_call' && 'ubuntu-24.04' || 'blacksmith-8vcpu-ubuntu-2404' }}
timeout-minutes: ${{ matrix.timeout_minutes }}
strategy:
@@ -608,6 +621,7 @@ jobs:
needs: [validate_selected_ref, prepare_docker_e2e_image]
if: inputs.include_release_path_suites && inputs.docker_lanes == ''
name: Docker E2E (${{ matrix.label }})
continue-on-error: ${{ inputs.advisory }}
runs-on: ${{ github.event_name == 'workflow_call' && 'ubuntu-24.04' || 'blacksmith-32vcpu-ubuntu-2404' }}
timeout-minutes: ${{ matrix.timeout_minutes }}
strategy:
@@ -876,6 +890,7 @@ jobs:
plan_docker_lane_groups:
needs: validate_selected_ref
if: inputs.docker_lanes != ''
continue-on-error: ${{ inputs.advisory }}
runs-on: ${{ github.event_name == 'workflow_call' && 'ubuntu-24.04' || 'blacksmith-4vcpu-ubuntu-2404' }}
timeout-minutes: 5
outputs:
@@ -903,6 +918,7 @@ jobs:
needs: [validate_selected_ref, prepare_docker_e2e_image, plan_docker_lane_groups]
if: inputs.docker_lanes != ''
name: Docker E2E targeted lanes (${{ matrix.group.label }})
continue-on-error: ${{ inputs.advisory }}
runs-on: ${{ github.event_name == 'workflow_call' && 'ubuntu-24.04' || 'blacksmith-32vcpu-ubuntu-2404' }}
timeout-minutes: 60
strategy:
@@ -1112,6 +1128,7 @@ jobs:
needs: [validate_selected_ref, prepare_docker_e2e_image]
if: inputs.include_openwebui && !inputs.include_release_path_suites && inputs.docker_lanes == ''
name: Docker E2E (openwebui)
continue-on-error: ${{ inputs.advisory }}
runs-on: ${{ github.event_name == 'workflow_call' && 'ubuntu-24.04' || 'blacksmith-32vcpu-ubuntu-2404' }}
timeout-minutes: 60
env:
@@ -1239,6 +1256,7 @@ jobs:
prepare_docker_e2e_image:
needs: validate_selected_ref
if: inputs.include_release_path_suites || inputs.include_openwebui || inputs.docker_lanes != ''
continue-on-error: ${{ inputs.advisory }}
runs-on: ${{ github.event_name == 'workflow_call' && 'ubuntu-24.04' || 'blacksmith-32vcpu-ubuntu-2404' }}
timeout-minutes: ${{ inputs.release_test_profile == 'full' && 90 || 60 }}
permissions:
@@ -1483,6 +1501,7 @@ jobs:
prepare_live_test_image:
needs: validate_selected_ref
if: inputs.include_live_suites && (inputs.live_suite_filter == '' || startsWith(inputs.live_suite_filter, 'live-') || startsWith(inputs.live_suite_filter, 'docker-live-models'))
continue-on-error: ${{ inputs.advisory }}
runs-on: ${{ github.event_name == 'workflow_call' && 'ubuntu-24.04' || 'blacksmith-32vcpu-ubuntu-2404' }}
timeout-minutes: 60
permissions:
@@ -1556,6 +1575,7 @@ jobs:
name: Docker live models (${{ matrix.provider_label }})
needs: [validate_selected_ref, prepare_live_test_image]
if: inputs.include_live_suites && inputs.live_model_providers == '' && (inputs.live_suite_filter == '' || inputs.live_suite_filter == 'docker-live-models')
continue-on-error: ${{ inputs.advisory }}
runs-on: ${{ github.event_name == 'workflow_call' && 'ubuntu-24.04' || 'blacksmith-32vcpu-ubuntu-2404' }}
timeout-minutes: 45
strategy:
@@ -1708,6 +1728,7 @@ jobs:
name: Docker live models (selected providers)
needs: [validate_selected_ref, prepare_live_test_image]
if: inputs.include_live_suites && inputs.live_model_providers != '' && (inputs.live_suite_filter == '' || inputs.live_suite_filter == 'docker-live-models')
continue-on-error: ${{ inputs.advisory }}
runs-on: ${{ github.event_name == 'workflow_call' && 'ubuntu-24.04' || 'blacksmith-32vcpu-ubuntu-2404' }}
timeout-minutes: 45
env:
@@ -1883,6 +1904,7 @@ jobs:
validate_live_provider_suites:
needs: validate_selected_ref
if: inputs.include_live_suites && !inputs.live_models_only && (inputs.live_suite_filter == '' || (startsWith(inputs.live_suite_filter, 'native-live-') && !startsWith(inputs.live_suite_filter, 'native-live-extensions-media') && inputs.live_suite_filter != 'native-live-extensions-a-k'))
continue-on-error: ${{ inputs.advisory }}
runs-on: ${{ github.event_name == 'workflow_call' && 'ubuntu-24.04' || 'blacksmith-8vcpu-ubuntu-2404' }}
timeout-minutes: ${{ matrix.timeout_minutes }}
strategy:
@@ -1911,7 +1933,7 @@ jobs:
- suite_id: native-live-src-gateway-profiles-anthropic-opus
suite_group: native-live-src-gateway-profiles-anthropic
label: Native live gateway profiles Anthropic Opus
command: OPENCLAW_LIVE_GATEWAY_PROVIDERS=anthropic OPENCLAW_LIVE_GATEWAY_MODELS=anthropic/claude-opus-4-7,anthropic/claude-opus-4-6 node .release-harness/scripts/test-live-shard.mjs native-live-src-gateway-profiles
command: OPENCLAW_LIVE_GATEWAY_PROVIDERS=anthropic OPENCLAW_LIVE_GATEWAY_MODELS=anthropic/claude-opus-4-7 node .release-harness/scripts/test-live-shard.mjs native-live-src-gateway-profiles
timeout_minutes: 30
profile_env_only: false
advisory: true
@@ -2204,6 +2226,7 @@ jobs:
name: Docker live suites (${{ matrix.label }})
needs: [validate_selected_ref, prepare_live_test_image]
if: inputs.include_live_suites && !inputs.live_models_only && (inputs.live_suite_filter == '' || startsWith(inputs.live_suite_filter, 'live-'))
continue-on-error: ${{ inputs.advisory }}
runs-on: ${{ github.event_name == 'workflow_call' && 'ubuntu-24.04' || 'blacksmith-32vcpu-ubuntu-2404' }}
timeout-minutes: ${{ matrix.timeout_minutes }}
strategy:
@@ -2423,6 +2446,7 @@ jobs:
name: Live media suites (${{ matrix.label }})
needs: validate_selected_ref
if: inputs.include_live_suites && !inputs.live_models_only && (inputs.live_suite_filter == '' || startsWith(inputs.live_suite_filter, 'native-live-extensions-media') || inputs.live_suite_filter == 'native-live-extensions-a-k')
continue-on-error: ${{ inputs.advisory }}
runs-on: ${{ github.event_name == 'workflow_call' && 'ubuntu-24.04' || 'blacksmith-8vcpu-ubuntu-2404' }}
container:
image: ghcr.io/openclaw/openclaw-live-media-runner:ubuntu-24.04

View File

@@ -88,6 +88,28 @@ jobs:
ref: ${{ inputs.tag }}
fetch-depth: 0
- name: Validate Tideclaw alpha preflight target
if: startsWith(github.ref, 'refs/heads/tideclaw/alpha/')
env:
RELEASE_REF: ${{ inputs.tag }}
WORKFLOW_REF: ${{ github.ref }}
run: |
set -euo pipefail
if [[ ! "${RELEASE_REF}" == *"-alpha."* && ! "${RELEASE_REF}" =~ ^[0-9a-fA-F]{40}$ ]]; then
echo "Tideclaw alpha preflight runs must target an alpha prerelease tag or SHA." >&2
exit 1
fi
if [[ ! "${WORKFLOW_REF}" =~ ^refs/heads/tideclaw/alpha/[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{4}Z$ ]]; then
echo "Tideclaw alpha preflight runs must run from tideclaw/alpha/YYYY-MM-DD-HHMMZ." >&2
exit 1
fi
alpha_branch="${WORKFLOW_REF#refs/heads/}"
git fetch --no-tags origin "+refs/heads/${alpha_branch}:refs/remotes/origin/${alpha_branch}"
if ! git merge-base --is-ancestor HEAD "refs/remotes/origin/${alpha_branch}"; then
echo "Alpha preflight target must be reachable from ${alpha_branch}." >&2
exit 1
fi
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
with:
@@ -191,7 +213,7 @@ jobs:
id: packed_tarball
env:
OPENCLAW_PREPACK_PREPARED: "1"
RELEASE_TAG: ${{ inputs.tag }}
RELEASE_REF: ${{ inputs.tag }}
RELEASE_NPM_DIST_TAG: ${{ inputs.npm_dist_tag }}
DEPENDENCY_EVIDENCE_DIR: ${{ steps.dependency_evidence.outputs.dir }}
run: |
@@ -259,6 +281,11 @@ jobs:
fi
RELEASE_SHA="$(git rev-parse HEAD)"
PACKAGE_VERSION="$(node -p "require('./package.json').version")"
if [[ "${RELEASE_REF}" =~ ^[0-9a-fA-F]{40}$ ]]; then
RELEASE_TAG="v${PACKAGE_VERSION}"
else
RELEASE_TAG="${RELEASE_REF}"
fi
TARBALL_NAME="$(basename "$PACK_PATH")"
TARBALL_SHA256="$(sha256sum "$PACK_PATH" | awk '{print $1}')"
ARTIFACT_DIR="$RUNNER_TEMP/openclaw-npm-preflight"
@@ -290,6 +317,7 @@ jobs:
);
NODE
echo "dir=$ARTIFACT_DIR" >> "$GITHUB_OUTPUT"
echo "release_tag=$RELEASE_TAG" >> "$GITHUB_OUTPUT"
- name: Verify prepared npm tarball install
env:
@@ -312,6 +340,14 @@ jobs:
path: ${{ steps.dependency_evidence.outputs.dir }}
if-no-files-found: error
- name: Upload dependency release evidence tag alias
if: ${{ steps.packed_tarball.outputs.release_tag != inputs.tag }}
uses: actions/upload-artifact@v7
with:
name: openclaw-release-dependency-evidence-${{ steps.packed_tarball.outputs.release_tag }}
path: ${{ steps.dependency_evidence.outputs.dir }}
if-no-files-found: error
- name: Upload prepared npm publish bundle
uses: actions/upload-artifact@v7
with:
@@ -319,19 +355,33 @@ jobs:
path: ${{ steps.packed_tarball.outputs.dir }}
if-no-files-found: error
- name: Upload prepared npm publish bundle tag alias
if: ${{ steps.packed_tarball.outputs.release_tag != inputs.tag }}
uses: actions/upload-artifact@v7
with:
name: openclaw-npm-preflight-${{ steps.packed_tarball.outputs.release_tag }}
path: ${{ steps.packed_tarball.outputs.dir }}
if-no-files-found: error
validate_publish_request:
if: ${{ !inputs.preflight_only }}
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Require main or release workflow ref for publish
- name: Require trusted workflow ref for publish
env:
RELEASE_TAG: ${{ inputs.tag }}
RELEASE_NPM_DIST_TAG: ${{ inputs.npm_dist_tag }}
WORKFLOW_REF: ${{ github.ref }}
run: |
set -euo pipefail
if [[ "${WORKFLOW_REF}" != "refs/heads/main" ]] && [[ ! "${WORKFLOW_REF}" =~ ^refs/heads/release/[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*$ ]]; then
echo "Real publish runs must be dispatched from main or release/YYYY.M.D. Use preflight_only=true for other branch validation."
tideclaw_alpha_publish=false
if [[ "${RELEASE_TAG}" == *"-alpha."* && "${RELEASE_NPM_DIST_TAG}" == "alpha" && "${WORKFLOW_REF}" =~ ^refs/heads/tideclaw/alpha/[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{4}Z$ ]]; then
tideclaw_alpha_publish=true
fi
if [[ "${WORKFLOW_REF}" != "refs/heads/main" ]] && [[ ! "${WORKFLOW_REF}" =~ ^refs/heads/release/[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*$ ]] && [[ "${tideclaw_alpha_publish}" != "true" ]]; then
echo "Real publish runs must be dispatched from main, release/YYYY.M.D, or a Tideclaw alpha branch for alpha prereleases. Use preflight_only=true for other branch validation."
exit 1
fi
@@ -387,6 +437,28 @@ jobs:
ref: refs/tags/${{ inputs.tag }}
fetch-depth: 0
- name: Validate Tideclaw alpha publish target
if: startsWith(github.ref, 'refs/heads/tideclaw/alpha/')
env:
RELEASE_TAG: ${{ inputs.tag }}
WORKFLOW_REF: ${{ github.ref }}
run: |
set -euo pipefail
if [[ ! "${RELEASE_TAG}" == *"-alpha."* ]]; then
echo "Tideclaw alpha publish runs must target an alpha prerelease tag." >&2
exit 1
fi
if [[ ! "${WORKFLOW_REF}" =~ ^refs/heads/tideclaw/alpha/[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{4}Z$ ]]; then
echo "Tideclaw alpha publish runs must run from tideclaw/alpha/YYYY-MM-DD-HHMMZ." >&2
exit 1
fi
alpha_branch="${WORKFLOW_REF#refs/heads/}"
git fetch --no-tags origin "+refs/heads/${alpha_branch}:refs/remotes/origin/${alpha_branch}"
if ! git merge-base --is-ancestor HEAD "refs/remotes/origin/${alpha_branch}"; then
echo "Alpha publish tag must be reachable from ${alpha_branch}." >&2
exit 1
fi
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
with:
@@ -427,13 +499,45 @@ jobs:
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}`);'
- name: Download prepared npm tarball
uses: actions/download-artifact@v8
with:
name: openclaw-npm-preflight-${{ inputs.tag }}
path: preflight-tarball
repository: ${{ github.repository }}
run-id: ${{ inputs.preflight_run_id }}
github-token: ${{ github.token }}
env:
GH_TOKEN: ${{ github.token }}
PREFLIGHT_RUN_ID: ${{ inputs.preflight_run_id }}
RELEASE_TAG: ${{ inputs.tag }}
run: |
set -euo pipefail
download_preflight_artifact() {
local preferred_name fallback_name
preferred_name="openclaw-npm-preflight-${RELEASE_TAG}"
rm -rf preflight-tarball
mkdir -p preflight-tarball
if gh run download "${PREFLIGHT_RUN_ID}" \
--repo "${GITHUB_REPOSITORY}" \
--name "${preferred_name}" \
--dir preflight-tarball; then
echo "Downloaded ${preferred_name}."
return 0
fi
echo "::warning::${preferred_name} not found; checking run artifacts for a single compatible preflight artifact."
mapfile -t matches < <(gh api -X GET "repos/${GITHUB_REPOSITORY}/actions/runs/${PREFLIGHT_RUN_ID}/artifacts" \
--jq '.artifacts[] | select(.expired != true) | .name' |
grep '^openclaw-npm-preflight-' || true)
if [[ "${#matches[@]}" != "1" ]]; then
echo "Expected ${preferred_name}, or exactly one openclaw-npm-preflight-* fallback artifact in run ${PREFLIGHT_RUN_ID}." >&2
printf 'Available preflight candidates:\n' >&2
printf -- '- %s\n' "${matches[@]:-<none>}" >&2
exit 1
fi
fallback_name="${matches[0]}"
gh run download "${PREFLIGHT_RUN_ID}" \
--repo "${GITHUB_REPOSITORY}" \
--name "${fallback_name}" \
--dir preflight-tarball
echo "Downloaded fallback preflight artifact ${fallback_name}."
}
download_preflight_artifact
- name: Download full release validation manifest
uses: actions/download-artifact@v8

View File

@@ -30,8 +30,8 @@ on:
required: false
default: false
type: boolean
live_gpt54:
description: Run the live OpenAI GPT 5.4 agent-turn lane
live_openai_candidate:
description: Run the live OpenAI GPT 5.5 agent-turn lane
required: false
default: false
type: boolean
@@ -57,7 +57,7 @@ env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
OCM_VERSION: v0.2.15
KOVA_REPOSITORY: openclaw/Kova
PERFORMANCE_MODEL_ID: gpt-5.4
PERFORMANCE_MODEL_ID: gpt-5.5
jobs:
kova:
@@ -82,8 +82,8 @@ jobs:
deep_profile: "true"
live: "false"
include_filters: "scenario:fresh-install scenario:gateway-performance scenario:agent-cold-warm-message"
- lane: live-gpt54
title: Kova live OpenAI GPT 5.4 agent turn
- lane: live-openai-candidate
title: Kova live OpenAI GPT 5.5 agent turn
auth: live
repeat: "1"
deep_profile: "false"
@@ -119,9 +119,9 @@ jobs:
run_lane=false
reason="deep_profile input is false"
fi
if [[ "$LANE_ID" == "live-gpt54" && "${{ github.event_name }}" != "schedule" && "${{ inputs.live_gpt54 || 'false' }}" != "true" ]]; then
if [[ "$LANE_ID" == "live-openai-candidate" && "${{ github.event_name }}" != "schedule" && "${{ inputs.live_openai_candidate || 'false' }}" != "true" ]]; then
run_lane=false
reason="live_gpt54 input is false"
reason="live_openai_candidate input is false"
fi
echo "run=$run_lane" >> "$GITHUB_OUTPUT"
if [[ "$run_lane" != "true" ]]; then
@@ -200,7 +200,7 @@ jobs:
chmod 0755 "$HOME/.local/bin/kova"
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
- name: Pin Kova OpenAI model to GPT 5.4
- name: Pin Kova OpenAI model to GPT 5.5
if: steps.lane.outputs.run == 'true'
shell: bash
run: |
@@ -244,7 +244,7 @@ jobs:
run: |
set -euo pipefail
if [[ -z "${OPENAI_API_KEY:-}" ]]; then
echo "OPENAI_API_KEY is not configured; live GPT 5.4 lane will be skipped." >> "$GITHUB_STEP_SUMMARY"
echo "OPENAI_API_KEY is not configured; live GPT 5.5 lane will be skipped." >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
kova setup --ci --json
@@ -561,7 +561,14 @@ jobs:
exit 0
fi
if [[ "$attempt" == "5" ]]; then
exit 1
{
echo "### Clawgrit report publish skipped"
echo
echo "Kova artifacts were uploaded, but publishing the optional clawgrit report failed after ${attempt} attempts."
echo "Check the \`CLAWGRIT_REPORTS_TOKEN\` secret or the reports repository permissions."
} >> "$GITHUB_STEP_SUMMARY"
echo "::warning::Kova artifacts uploaded, but optional clawgrit report publish failed after ${attempt} attempts."
exit 0
fi
sleep $((attempt * 2))
git -C "$reports_root" fetch --depth=1 origin main

View File

@@ -113,13 +113,21 @@ jobs:
release_package_spec: ${{ steps.inputs.outputs.release_package_spec }}
package_acceptance_package_spec: ${{ steps.inputs.outputs.package_acceptance_package_spec }}
steps:
- name: Require main or release workflow ref for release checks
- name: Require trusted workflow ref for release checks
env:
RELEASE_REF: ${{ inputs.ref }}
WORKFLOW_REF: ${{ github.ref }}
run: |
set -euo pipefail
if [[ "${WORKFLOW_REF}" != "refs/heads/main" ]] && [[ ! "${WORKFLOW_REF}" =~ ^refs/heads/release/[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*$ ]] && [[ ! "${WORKFLOW_REF}" =~ ^refs/heads/release-ci/[0-9a-f]{12}-[0-9]+$ ]]; then
echo "Release checks must be dispatched from main, release/YYYY.M.D, or a Full Release Validation release-ci/<sha>-<timestamp> ref so workflow logic and secrets stay controlled." >&2
tideclaw_alpha_check=false
if [[ "${WORKFLOW_REF}" =~ ^refs/heads/tideclaw/alpha/[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{4}Z$ ]]; then
workflow_branch="${WORKFLOW_REF#refs/heads/}"
if [[ "${RELEASE_REF}" == *"-alpha."* || "${RELEASE_REF}" =~ ^[0-9a-fA-F]{40}$ || "${RELEASE_REF}" == "${workflow_branch}" || "${RELEASE_REF}" == "refs/heads/${workflow_branch}" ]]; then
tideclaw_alpha_check=true
fi
fi
if [[ "${WORKFLOW_REF}" != "refs/heads/main" ]] && [[ ! "${WORKFLOW_REF}" =~ ^refs/heads/release/[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*$ ]] && [[ ! "${WORKFLOW_REF}" =~ ^refs/heads/release-ci/[0-9a-f]{12}-[0-9]+$ ]] && [[ "${tideclaw_alpha_check}" != "true" ]]; then
echo "Release checks must be dispatched from main, release/YYYY.M.D, a Full Release Validation release-ci/<sha>-<timestamp> ref, or a Tideclaw alpha branch for alpha prereleases." >&2
exit 1
fi
@@ -219,6 +227,25 @@ jobs:
fi
echo "sha=${selected_sha,,}" >> "$GITHUB_OUTPUT"
- name: Validate Tideclaw alpha target matches workflow branch
if: startsWith(github.ref, 'refs/heads/tideclaw/alpha/')
working-directory: workflow
env:
SELECTED_SHA: ${{ steps.ref.outputs.sha }}
WORKFLOW_REF: ${{ github.ref }}
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
echo "Tideclaw alpha release checks must run from tideclaw/alpha/YYYY-MM-DD-HHMMZ." >&2
exit 1
fi
alpha_branch="${WORKFLOW_REF#refs/heads/}"
git fetch --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
fi
- name: Capture selected inputs
id: inputs
env:
@@ -507,6 +534,7 @@ jobs:
permissions: read-all
uses: ./.github/workflows/openclaw-cross-os-release-checks-reusable.yml
with:
advisory: ${{ startsWith(github.ref, 'refs/heads/tideclaw/alpha/') }}
ref: ${{ needs.resolve_target.outputs.revision }}
provider: ${{ needs.resolve_target.outputs.provider }}
mode: ${{ needs.resolve_target.outputs.mode }}
@@ -515,7 +543,7 @@ jobs:
candidate_file_name: openclaw-current.tgz
candidate_version: ${{ needs.prepare_release_package.outputs.package_version }}
candidate_source_sha: ${{ needs.prepare_release_package.outputs.source_sha }}
openai_model: openai/gpt-5.4
openai_model: openai/gpt-5.5
ubuntu_runner: ubuntu-24.04
windows_runner: windows-2025
macos_runner: macos-26
@@ -538,6 +566,7 @@ jobs:
pull-requests: read
uses: ./.github/workflows/openclaw-live-and-e2e-checks-reusable.yml
with:
advisory: ${{ startsWith(github.ref, 'refs/heads/tideclaw/alpha/') }}
ref: ${{ needs.resolve_target.outputs.revision }}
include_repo_e2e: true
include_release_path_suites: false
@@ -603,6 +632,7 @@ jobs:
pull-requests: read
uses: ./.github/workflows/openclaw-live-and-e2e-checks-reusable.yml
with:
advisory: ${{ startsWith(github.ref, 'refs/heads/tideclaw/alpha/') }}
ref: ${{ needs.resolve_target.outputs.revision }}
include_repo_e2e: false
include_release_path_suites: true
@@ -623,6 +653,7 @@ jobs:
pull-requests: read
uses: ./.github/workflows/package-acceptance.yml
with:
advisory: ${{ startsWith(github.ref, 'refs/heads/tideclaw/alpha/') }}
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' }}
@@ -633,7 +664,7 @@ jobs:
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
telegram_scenarios: telegram-help-command,telegram-commands-command,telegram-tools-compact-command,telegram-whoami-command,telegram-status-command,telegram-other-bot-command-gating,telegram-context-command,telegram-mentioned-message-reply,telegram-reply-chain-exact-marker,telegram-stream-final-single-message,telegram-long-final-reuses-preview,telegram-mention-gating
telegram_scenarios: telegram-help-command,telegram-commands-command,telegram-tools-compact-command,telegram-whoami-command,telegram-status-command,telegram-other-bot-command-gating,telegram-context-command,telegram-mentioned-message-reply,telegram-long-final-reuses-preview,telegram-mention-gating
secrets:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL }}
@@ -697,9 +728,9 @@ jobs:
matrix:
include:
- lane: candidate
output_dir: gpt54
output_dir: openai-candidate
- lane: baseline
output_dir: opus46
output_dir: anthropic-baseline
env:
QA_PARITY_CONCURRENCY: "1"
OPENCLAW_QA_TRANSPORT_READY_TIMEOUT_MS: "180000"
@@ -745,7 +776,7 @@ jobs:
;;
baseline)
model="anthropic/claude-opus-4-7"
alt_model="anthropic/claude-sonnet-4-7"
alt_model="anthropic/claude-sonnet-4-6"
;;
*)
echo "Unknown QA parity lane: ${QA_PARITY_LANE}" >&2
@@ -814,8 +845,8 @@ jobs:
run: |
pnpm openclaw qa parity-report \
--repo-root . \
--candidate-summary .artifacts/qa-e2e/gpt54/qa-suite-summary.json \
--baseline-summary .artifacts/qa-e2e/opus46/qa-suite-summary.json \
--candidate-summary .artifacts/qa-e2e/openai-candidate/qa-suite-summary.json \
--baseline-summary .artifacts/qa-e2e/anthropic-baseline/qa-suite-summary.json \
--candidate-label "${OPENCLAW_CI_OPENAI_MODEL}" \
--baseline-label anthropic/claude-opus-4-7 \
--output-dir .artifacts/qa-e2e/parity
@@ -829,6 +860,152 @@ jobs:
retention-days: 14
if-no-files-found: warn
qa_lab_runtime_parity_release_checks:
name: Run QA Lab runtime parity lane
needs: [resolve_target]
if: contains(fromJSON('["all","qa","qa-parity"]'), needs.resolve_target.outputs.rerun_group)
continue-on-error: true
runs-on: blacksmith-8vcpu-ubuntu-2404
timeout-minutes: 30
permissions:
contents: read
env:
QA_PARITY_CONCURRENCY: "1"
OPENCLAW_QA_TRANSPORT_READY_TIMEOUT_MS: "180000"
OPENAI_API_KEY: ""
ANTHROPIC_API_KEY: ""
OPENCLAW_LIVE_OPENAI_KEY: ""
OPENCLAW_LIVE_ANTHROPIC_KEY: ""
OPENCLAW_LIVE_GEMINI_KEY: ""
OPENCLAW_LIVE_SETUP_TOKEN_VALUE: ""
OPENCLAW_BUILD_PRIVATE_QA: "1"
OPENCLAW_ENABLE_PRIVATE_QA_CLI: "1"
steps:
- name: Checkout selected ref
uses: actions/checkout@v6
with:
persist-credentials: false
ref: ${{ needs.resolve_target.outputs.revision }}
fetch-depth: 1
- 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: Build private QA runtime
env:
NODE_OPTIONS: --max-old-space-size=8192
run: pnpm build
- name: Run runtime parity lane
id: runtime_parity_lane
run: |
set -euo pipefail
pnpm openclaw qa suite \
--provider-mode mock-openai \
--parity-pack agentic \
--concurrency "${QA_PARITY_CONCURRENCY}" \
--model "${OPENCLAW_CI_OPENAI_MODEL}" \
--alt-model "openai/gpt-5.5-alt" \
--runtime-pair pi,codex \
--output-dir ".artifacts/qa-e2e/runtime-parity"
- name: Run standard runtime parity tier
if: ${{ always() && steps.runtime_parity_lane.outcome != 'skipped' && steps.runtime_parity_lane.outcome != 'cancelled' }}
run: |
set -euo pipefail
pnpm openclaw qa suite \
--provider-mode mock-openai \
--runtime-parity-tier standard \
--concurrency "${QA_PARITY_CONCURRENCY}" \
--model "${OPENCLAW_CI_OPENAI_MODEL}" \
--alt-model "openai/gpt-5.5-alt" \
--runtime-pair pi,codex \
--output-dir ".artifacts/qa-e2e/runtime-parity-standard"
- name: Generate runtime parity report
if: always()
run: |
set -euo pipefail
pnpm openclaw qa parity-report \
--repo-root . \
--runtime-axis \
--summary .artifacts/qa-e2e/runtime-parity/qa-suite-summary.json \
--output-dir .artifacts/qa-e2e/runtime-parity-report
- name: Generate standard runtime parity report
if: always()
run: |
set -euo pipefail
pnpm openclaw qa parity-report \
--repo-root . \
--runtime-axis \
--summary .artifacts/qa-e2e/runtime-parity-standard/qa-suite-summary.json \
--output-dir .artifacts/qa-e2e/runtime-parity-standard-report
- name: Upload runtime parity artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: release-qa-runtime-parity-${{ needs.resolve_target.outputs.revision }}
path: .artifacts/qa-e2e/
retention-days: 14
if-no-files-found: warn
runtime_tool_coverage_release_checks:
name: Enforce QA Lab runtime tool coverage
needs: [resolve_target, qa_lab_runtime_parity_release_checks]
if: always() && contains(fromJSON('["all","qa","qa-parity"]'), needs.resolve_target.outputs.rerun_group)
runs-on: ubuntu-24.04
timeout-minutes: 15
permissions:
contents: read
actions: read
env:
OPENCLAW_BUILD_PRIVATE_QA: "1"
OPENCLAW_ENABLE_PRIVATE_QA_CLI: "1"
steps:
- name: Checkout selected ref
uses: actions/checkout@v6
with:
persist-credentials: false
ref: ${{ needs.resolve_target.outputs.revision }}
fetch-depth: 1
- 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: Download runtime parity artifacts
uses: actions/download-artifact@v4
with:
name: release-qa-runtime-parity-${{ needs.resolve_target.outputs.revision }}
path: .artifacts/qa-e2e/
- name: Enforce standard runtime tool coverage
run: |
set -euo pipefail
pnpm openclaw qa coverage \
--repo-root . \
--tools \
--summary .artifacts/qa-e2e/runtime-parity-standard/qa-suite-summary.json \
--output .artifacts/qa-e2e/runtime-parity-standard-report/qa-runtime-tool-coverage-report.md
- name: Upload runtime tool coverage artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: release-qa-runtime-tool-coverage-${{ needs.resolve_target.outputs.revision }}
path: .artifacts/qa-e2e/runtime-parity-standard-report/
retention-days: 14
if-no-files-found: warn
qa_live_matrix_release_checks:
name: Run QA Lab live Matrix lane
needs: [resolve_target]
@@ -1108,6 +1285,9 @@ jobs:
continue-on-error: true
runs-on: ubuntu-24.04
timeout-minutes: 60
concurrency:
group: qa-live-whatsapp-shared
cancel-in-progress: false
permissions:
contents: read
pull-requests: read
@@ -1304,6 +1484,8 @@ jobs:
- package_acceptance_release_checks
- qa_lab_parity_lane_release_checks
- qa_lab_parity_report_release_checks
- qa_lab_runtime_parity_release_checks
- runtime_tool_coverage_release_checks
- qa_live_matrix_release_checks
- qa_live_telegram_release_checks
- qa_live_discord_release_checks
@@ -1316,9 +1498,15 @@ jobs:
steps:
- name: Verify release check results
shell: bash
env:
WORKFLOW_REF: ${{ github.ref }}
run: |
set -euo pipefail
failed=0
tideclaw_alpha=false
if [[ "${WORKFLOW_REF}" =~ ^refs/heads/tideclaw/alpha/[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{4}Z$ ]]; then
tideclaw_alpha=true
fi
for item in \
"prepare_release_package=${{ needs.prepare_release_package.result }}" \
"install_smoke_release_checks=${{ needs.install_smoke_release_checks.result }}" \
@@ -1328,6 +1516,8 @@ jobs:
"package_acceptance_release_checks=${{ needs.package_acceptance_release_checks.result }}" \
"qa_lab_parity_lane_release_checks=${{ needs.qa_lab_parity_lane_release_checks.result }}" \
"qa_lab_parity_report_release_checks=${{ needs.qa_lab_parity_report_release_checks.result }}" \
"qa_lab_runtime_parity_release_checks=${{ needs.qa_lab_runtime_parity_release_checks.result }}" \
"runtime_tool_coverage_release_checks=${{ needs.runtime_tool_coverage_release_checks.result }}" \
"qa_live_matrix_release_checks=${{ needs.qa_live_matrix_release_checks.result }}" \
"qa_live_telegram_release_checks=${{ needs.qa_live_telegram_release_checks.result }}" \
"qa_live_discord_release_checks=${{ needs.qa_live_discord_release_checks.result }}" \
@@ -1337,6 +1527,15 @@ jobs:
name="${item%%=*}"
result="${item#*=}"
if [[ "$result" != "success" && "$result" != "skipped" ]]; then
if [[ "$tideclaw_alpha" == "true" ]]; then
case "$name" in
prepare_release_package|install_smoke_release_checks) ;;
*)
echo "::warning::${name} ended with ${result}; Tideclaw alpha treats non-package-safety release-check lanes as advisory."
continue
;;
esac
fi
if [[ "$name" == qa_* ]]; then
echo "::warning::${name} ended with ${result}; QA release-check lanes are advisory and do not block release validation."
continue

View File

@@ -15,6 +15,10 @@ on:
description: Successful Full Release Validation run id for this tag/SHA, required when publish_openclaw_npm=true
required: false
type: string
npm_telegram_run_id:
description: Optional successful NPM Telegram Beta E2E run id to include in final release evidence
required: false
type: string
npm_dist_tag:
description: npm dist-tag for the OpenClaw package
required: true
@@ -76,6 +80,7 @@ jobs:
timeout-minutes: 20
outputs:
sha: ${{ steps.manifest.outputs.sha || steps.ref.outputs.sha }}
preflight_artifact_name: ${{ steps.preflight_artifact.outputs.name }}
steps:
- name: Validate inputs
env:
@@ -110,8 +115,12 @@ jobs:
echo "publish_openclaw_npm=true requires full_release_validation_run_id." >&2
exit 1
fi
if [[ "${PUBLISH_OPENCLAW_NPM}" == "true" && "${WORKFLOW_REF}" != "refs/heads/main" && ! "${WORKFLOW_REF}" =~ ^refs/heads/release/[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*$ ]]; then
echo "publish_openclaw_npm=true requires dispatching this workflow from main or release/YYYY.M.D." >&2
tideclaw_alpha_publish=false
if [[ "${RELEASE_TAG}" == *"-alpha."* && "${RELEASE_NPM_DIST_TAG}" == "alpha" && "${WORKFLOW_REF}" =~ ^refs/heads/tideclaw/alpha/[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{4}Z$ ]]; then
tideclaw_alpha_publish=true
fi
if [[ "${PUBLISH_OPENCLAW_NPM}" == "true" && "${WORKFLOW_REF}" != "refs/heads/main" && ! "${WORKFLOW_REF}" =~ ^refs/heads/release/[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*$ && "${tideclaw_alpha_publish}" != "true" ]]; then
echo "publish_openclaw_npm=true requires dispatching this workflow from main, release/YYYY.M.D, or a Tideclaw alpha branch for alpha prereleases." >&2
exit 1
fi
if [[ "${PLUGIN_PUBLISH_SCOPE}" == "selected" && -z "${PLUGINS}" ]]; then
@@ -131,14 +140,43 @@ jobs:
esac
- name: Download OpenClaw npm preflight manifest
id: preflight_artifact
if: ${{ inputs.publish_openclaw_npm }}
uses: actions/download-artifact@v8
with:
name: openclaw-npm-preflight-${{ inputs.tag }}
path: ${{ runner.temp }}/openclaw-npm-preflight-manifest
repository: ${{ github.repository }}
run-id: ${{ inputs.preflight_run_id }}
github-token: ${{ github.token }}
env:
GH_TOKEN: ${{ github.token }}
PREFLIGHT_RUN_ID: ${{ inputs.preflight_run_id }}
RELEASE_TAG: ${{ inputs.tag }}
run: |
set -euo pipefail
preferred_name="openclaw-npm-preflight-${RELEASE_TAG}"
preflight_dir="${RUNNER_TEMP}/openclaw-npm-preflight-manifest"
rm -rf "${preflight_dir}"
mkdir -p "${preflight_dir}"
if gh run download "${PREFLIGHT_RUN_ID}" \
--repo "${GITHUB_REPOSITORY}" \
--name "${preferred_name}" \
--dir "${preflight_dir}"; then
echo "name=${preferred_name}" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "::warning::${preferred_name} not found; checking run artifacts for a single compatible preflight artifact."
mapfile -t matches < <(gh api -X GET "repos/${GITHUB_REPOSITORY}/actions/runs/${PREFLIGHT_RUN_ID}/artifacts" \
--jq '.artifacts[] | select(.expired != true) | .name' |
grep '^openclaw-npm-preflight-' || true)
if [[ "${#matches[@]}" != "1" ]]; then
echo "Expected ${preferred_name}, or exactly one openclaw-npm-preflight-* fallback artifact in run ${PREFLIGHT_RUN_ID}." >&2
printf 'Available preflight candidates:\n' >&2
printf -- '- %s\n' "${matches[@]:-<none>}" >&2
exit 1
fi
fallback_name="${matches[0]}"
gh run download "${PREFLIGHT_RUN_ID}" \
--repo "${GITHUB_REPOSITORY}" \
--name "${fallback_name}" \
--dir "${preflight_dir}"
echo "name=${fallback_name}" >> "$GITHUB_OUTPUT"
- name: Download full release validation manifest
if: ${{ inputs.publish_openclaw_npm }}
@@ -245,7 +283,10 @@ jobs:
exit 1
fi
- name: Validate release tag is reachable from main or release branch
- name: Validate release tag is reachable from a trusted release branch
env:
RELEASE_TAG: ${{ inputs.tag }}
WORKFLOW_REF_NAME: ${{ github.ref_name }}
run: |
set -euo pipefail
git fetch --no-tags origin \
@@ -259,7 +300,17 @@ jobs:
exit 0
fi
done < <(git for-each-ref --format='%(refname)' refs/remotes/origin/release)
echo "Release tag must point to a commit reachable from main or release/*." >&2
if [[ "${RELEASE_TAG}" == *"-alpha."* ]]; then
if [[ ! "${WORKFLOW_REF_NAME}" =~ ^tideclaw/alpha/[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{4}Z$ ]]; then
echo "Alpha publish tags must be dispatched from tideclaw/alpha/YYYY-MM-DD-HHMMZ." >&2
exit 1
fi
git fetch --no-tags origin "+refs/heads/${WORKFLOW_REF_NAME}:refs/remotes/origin/${WORKFLOW_REF_NAME}"
if git merge-base --is-ancestor HEAD "refs/remotes/origin/${WORKFLOW_REF_NAME}"; then
exit 0
fi
fi
echo "Release tag must point to a commit reachable from main, release/*, or the matching Tideclaw alpha branch for alpha prereleases." >&2
exit 1
- name: Summarize release target
@@ -293,6 +344,12 @@ jobs:
fetch-depth: 1
persist-credentials: false
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
with:
install-bun: "false"
cache-key-suffix: release-publish
- name: Dispatch publish workflows
env:
GH_TOKEN: ${{ github.token }}
@@ -306,6 +363,9 @@ jobs:
PLUGINS: ${{ inputs.plugins }}
PUBLISH_OPENCLAW_NPM: ${{ inputs.publish_openclaw_npm && 'true' || 'false' }}
WAIT_FOR_CLAWHUB: ${{ inputs.wait_for_clawhub && 'true' || 'false' }}
PREFLIGHT_ARTIFACT_NAME: ${{ needs.resolve_release_target.outputs.preflight_artifact_name }}
NPM_TELEGRAM_RUN_ID: ${{ inputs.npm_telegram_run_id }}
POSTPUBLISH_EVIDENCE_DIR: ${{ runner.temp }}/openclaw-release-postpublish-evidence
run: |
set -euo pipefail
@@ -314,7 +374,10 @@ jobs:
shift
local before_json dispatch_output run_id
before_json="$(gh run list --repo "$GITHUB_REPOSITORY" --workflow "$workflow" --event workflow_dispatch --limit 100 --json databaseId --jq '[.[].databaseId]')"
before_json="$(gh api -X GET "repos/${GITHUB_REPOSITORY}/actions/workflows/${workflow}/runs" \
-F event=workflow_dispatch \
-F per_page=100 \
--jq '[.workflow_runs[].id]')"
dispatch_output="$(gh workflow run --repo "$GITHUB_REPOSITORY" "$workflow" --ref "$CHILD_WORKFLOW_REF" "$@" 2>&1)"
printf '%s\n' "$dispatch_output" >&2
@@ -327,8 +390,10 @@ jobs:
if [[ -z "$run_id" ]]; then
for _ in $(seq 1 60); do
run_id="$(
BEFORE_IDS="$before_json" gh run list --repo "$GITHUB_REPOSITORY" --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'
BEFORE_IDS="$before_json" gh api -X GET "repos/${GITHUB_REPOSITORY}/actions/workflows/${workflow}/runs" \
-F event=workflow_dispatch \
-F per_page=50 \
--jq '.workflow_runs | map({databaseId:.id, createdAt:.created_at}) | map(select(.databaseId as $id | (env.BEFORE_IDS | fromjson | index($id) | not))) | sort_by(.createdAt) | reverse | .[0].databaseId // empty'
)"
if [[ -n "$run_id" ]]; then
break
@@ -349,6 +414,73 @@ jobs:
printf '%s\n' "${run_id}"
}
print_pending_deployments() {
local workflow="$1"
local run_id="$2"
local pending_json
pending_json="$(gh api -X GET "repos/${GITHUB_REPOSITORY}/actions/runs/${run_id}/pending_deployments" 2>/dev/null || true)"
if [[ -z "${pending_json}" ]] || ! printf '%s' "${pending_json}" | jq -e 'length > 0' >/dev/null 2>&1; then
return 0
fi
echo "${workflow} pending environment approval:"
while IFS=$'\t' read -r env_id env_name can_approve; do
echo "- env=${env_name} canApprove=${can_approve}"
echo " approve: gh api -X POST repos/${GITHUB_REPOSITORY}/actions/runs/${run_id}/pending_deployments -F 'environment_ids[]=${env_id}' -f state=approved -f comment='Approve release gate'"
done < <(printf '%s' "${pending_json}" | jq -r '.[] | [.environment.id, .environment.name, .current_user_can_approve] | @tsv')
}
approve_pending_deployments() {
local workflow="$1"
local run_id="$2"
local pending_json approved
pending_json="$(gh api -X GET "repos/${GITHUB_REPOSITORY}/actions/runs/${run_id}/pending_deployments" 2>/dev/null || true)"
if [[ -z "${pending_json}" ]] || ! printf '%s' "${pending_json}" | jq -e 'length > 0' >/dev/null 2>&1; then
return 0
fi
approved=0
while IFS=$'\t' read -r env_id env_name; do
if [[ -z "${env_id}" ]]; then
continue
fi
echo "${workflow}: approving pending environment ${env_name} (${env_id})"
gh api -X POST "repos/${GITHUB_REPOSITORY}/actions/runs/${run_id}/pending_deployments" \
-F "environment_ids[]=${env_id}" \
-f state=approved \
-f comment="Approve release gate from OpenClaw Release Publish wrapper" >/dev/null
approved=1
done < <(printf '%s' "${pending_json}" | jq -r '.[] | select(.current_user_can_approve == true) | [.environment.id, .environment.name] | @tsv')
if [[ "${approved}" == "1" ]]; then
echo "${workflow}: approved available pending environment gates"
fi
}
print_failed_run_summary() {
local run_id="$1"
local failed_json
failed_json="$(gh run view --repo "$GITHUB_REPOSITORY" "$run_id" --json jobs \
--jq '.jobs[] | select(.conclusion != "success" and .conclusion != "skipped") | {databaseId, name, conclusion, url}' || true)"
if [[ -z "${failed_json}" ]]; then
return 0
fi
echo "Failed child job summary:"
printf '%s\n' "${failed_json}"
while IFS=$'\t' read -r job_id job_name; do
if [[ -z "${job_id}" ]]; then
continue
fi
echo "--- ${job_name} (${job_id}) log tail ---"
gh run view --repo "$GITHUB_REPOSITORY" "$run_id" --job "${job_id}" --log 2>/dev/null |
tail -200 || true
done < <(printf '%s\n' "${failed_json}" | jq -r '[.databaseId, .name] | @tsv' 2>/dev/null || true)
}
wait_for_run() {
local workflow="$1"
local run_id="$2"
@@ -366,6 +498,8 @@ jobs:
state="${status}:${updated_at}"
if [[ "$state" != "$last_state" ]]; then
echo "${workflow} still ${status} (updated ${updated_at}): ${url}"
print_pending_deployments "${workflow}" "${run_id}"
approve_pending_deployments "${workflow}" "${run_id}"
last_state="$state"
fi
sleep 30
@@ -393,7 +527,7 @@ jobs:
echo "- ${workflow}: ${conclusion} in ${duration_label} (${url})"
} >> "$GITHUB_STEP_SUMMARY"
if [[ "$conclusion" != "success" ]]; then
gh run view --repo "$GITHUB_REPOSITORY" "$run_id" --json jobs --jq '.jobs[] | select(.conclusion != "success" and .conclusion != "skipped") | {name, conclusion, url}' || true
print_failed_run_summary "${run_id}"
return 1
fi
}
@@ -466,17 +600,18 @@ jobs:
}
upload_dependency_evidence_release_asset() {
local release_version download_dir asset_path asset_name
local release_version download_dir asset_path asset_name artifact_name
release_version="${RELEASE_TAG#v}"
download_dir="${RUNNER_TEMP}/openclaw-release-dependency-evidence-asset"
asset_name="openclaw-${release_version}-dependency-evidence.zip"
asset_path="${RUNNER_TEMP}/${asset_name}"
artifact_name="${PREFLIGHT_ARTIFACT_NAME:-openclaw-npm-preflight-${RELEASE_TAG}}"
rm -rf "${download_dir}" "${asset_path}"
mkdir -p "${download_dir}"
gh run download "${PREFLIGHT_RUN_ID}" \
--repo "${GITHUB_REPOSITORY}" \
--name "openclaw-npm-preflight-${RELEASE_TAG}" \
--name "${artifact_name}" \
--dir "${download_dir}"
if [[ ! -d "${download_dir}/dependency-evidence" ]]; then
@@ -492,6 +627,42 @@ jobs:
echo "- Dependency evidence asset: \`${asset_name}\`" >> "$GITHUB_STEP_SUMMARY"
}
verify_published_release() {
local release_version evidence_path
local -a verify_args
release_version="${RELEASE_TAG#v}"
evidence_path="${POSTPUBLISH_EVIDENCE_DIR}/release-postpublish-evidence.json"
mkdir -p "${POSTPUBLISH_EVIDENCE_DIR}"
verify_args=(
release:verify-beta
--
"${release_version}"
--tag "${RELEASE_TAG}"
--dist-tag "${RELEASE_NPM_DIST_TAG}"
--repo "${GITHUB_REPOSITORY}"
--workflow-ref "${CHILD_WORKFLOW_REF}"
--full-release-validation-run "${FULL_RELEASE_VALIDATION_RUN_ID}"
--plugin-npm-run "${plugin_npm_run_id}"
--plugin-clawhub-run "${plugin_clawhub_run_id}"
--openclaw-npm-run "${openclaw_npm_run_id}"
--evidence-out "${evidence_path}"
)
if [[ -n "${PLUGINS// }" ]]; then
verify_args+=(--plugins "${PLUGINS}")
fi
if [[ -n "${NPM_TELEGRAM_RUN_ID// }" ]]; then
verify_args+=(--npm-telegram-run "${NPM_TELEGRAM_RUN_ID}")
fi
pnpm "${verify_args[@]}"
{
echo "- Postpublish verification: passed"
echo "- Postpublish evidence: \`${evidence_path}\`"
} >> "$GITHUB_STEP_SUMMARY"
}
{
echo "### Publish sequence"
echo
@@ -500,11 +671,11 @@ jobs:
echo "- Release SHA: \`${TARGET_SHA}\`"
echo "- Plugin npm and ClawHub publish: dispatched in parallel"
if [[ "${PUBLISH_OPENCLAW_NPM}" == "true" ]]; then
echo "- OpenClaw npm publish: starts after plugin npm succeeds; ClawHub may still be running"
echo "- OpenClaw npm publish: starts after plugin npm succeeds; final verification waits for ClawHub"
else
echo "- OpenClaw npm publish: skipped by input"
fi
if [[ "${WAIT_FOR_CLAWHUB}" == "true" ]]; then
if [[ "${WAIT_FOR_CLAWHUB}" == "true" || "${PUBLISH_OPENCLAW_NPM}" == "true" ]]; then
echo "- Workflow completion waits for ClawHub"
else
echo "- Workflow completion does not wait for ClawHub; monitor the dispatched ClawHub run separately"
@@ -546,7 +717,7 @@ jobs:
clawhub_result=""
clawhub_pid=""
if [[ "${WAIT_FOR_CLAWHUB}" == "true" ]]; then
if [[ "${WAIT_FOR_CLAWHUB}" == "true" || "${PUBLISH_OPENCLAW_NPM}" == "true" ]]; then
clawhub_result="$RUNNER_TEMP/clawhub-result.txt"
wait_run_pid=""
wait_for_run_background plugin-clawhub-release.yml "${plugin_clawhub_run_id}" "${clawhub_result}"
@@ -565,23 +736,39 @@ jobs:
fi
failed=0
if [[ -n "${clawhub_pid}" ]] && ! wait "${clawhub_pid}"; then
failed=1
fi
openclaw_failed=0
if [[ -n "${openclaw_pid}" ]] && ! wait "${openclaw_pid}"; then
failed=1
openclaw_failed=1
fi
if [[ -n "${openclaw_result}" && -f "${openclaw_result}" && "$(cat "${openclaw_result}")" != "success" ]]; then
failed=1
openclaw_failed=1
fi
if [[ -n "${openclaw_npm_run_id}" && "${openclaw_failed}" == "0" ]]; then
create_or_update_github_release
upload_dependency_evidence_release_asset
fi
if [[ -n "${clawhub_pid}" ]] && ! wait "${clawhub_pid}"; then
failed=1
fi
if [[ -f "${clawhub_result}" && "$(cat "${clawhub_result}")" != "success" ]]; then
failed=1
fi
if [[ -n "${openclaw_result}" && -f "${openclaw_result}" && "$(cat "${openclaw_result}")" != "success" ]]; then
failed=1
if [[ "${failed}" == "0" && -n "${openclaw_npm_run_id}" ]]; then
verify_published_release
fi
if [[ "${failed}" != "0" ]]; then
exit 1
fi
if [[ -n "${openclaw_npm_run_id}" ]]; then
create_or_update_github_release
upload_dependency_evidence_release_asset
fi
- name: Upload postpublish evidence
if: ${{ always() }}
uses: actions/upload-artifact@v7
with:
name: openclaw-release-postpublish-evidence-${{ inputs.tag }}
path: ${{ runner.temp }}/openclaw-release-postpublish-evidence
if-no-files-found: ignore

View File

@@ -93,8 +93,18 @@ on:
required: false
default: ""
type: string
advisory:
description: Treat acceptance failures as advisory for the caller
required: false
default: false
type: boolean
workflow_call:
inputs:
advisory:
description: Treat acceptance failures as advisory for the caller
required: false
default: false
type: boolean
workflow_ref:
description: Trusted repo ref for workflow scripts and Docker E2E harness
required: false
@@ -509,6 +519,7 @@ jobs:
needs: resolve_package
uses: ./.github/workflows/openclaw-live-and-e2e-checks-reusable.yml
with:
advisory: ${{ inputs.advisory }}
ref: ${{ needs.resolve_package.outputs.package_source_sha || inputs.workflow_ref }}
include_repo_e2e: false
include_release_path_suites: ${{ needs.resolve_package.outputs.include_release_path_suites == 'true' }}
@@ -573,6 +584,7 @@ jobs:
if: needs.resolve_package.outputs.telegram_enabled == 'true'
uses: ./.github/workflows/npm-telegram-beta-e2e.yml
with:
advisory: ${{ inputs.advisory }}
package_spec: ${{ inputs.package_spec }}
package_artifact_name: ${{ needs.resolve_package.outputs.package_artifact_name }}
package_label: openclaw@${{ needs.resolve_package.outputs.package_version }}
@@ -599,6 +611,7 @@ jobs:
shell: bash
run: |
set -euo pipefail
advisory="${{ inputs.advisory }}"
failed=0
for item in \
"resolve_package=${RESOLVE_RESULT}" \
@@ -608,6 +621,10 @@ jobs:
name="${item%%=*}"
result="${item#*=}"
if [[ "$result" != "success" && "$result" != "skipped" ]]; then
if [[ "$advisory" == "true" && "$name" != "resolve_package" ]]; then
echo "::warning::${name} ended with ${result}; package acceptance is advisory for this caller."
continue
fi
echo "::error::${name} ended with ${result}"
failed=1
fi

View File

@@ -16,7 +16,7 @@ on:
required: false
type: string
ref:
description: Commit SHA on main or a release branch to publish from; defaults to the workflow ref
description: Commit SHA on main, a release branch, or the matching Tideclaw alpha branch to publish from; defaults to the workflow ref
required: false
default: ""
type: string
@@ -82,7 +82,9 @@ jobs:
fi
echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
- name: Validate ref is on main or a release branch
- name: Validate ref is on a trusted publish branch
env:
WORKFLOW_REF: ${{ github.ref }}
run: |
set -euo pipefail
if git merge-base --is-ancestor HEAD origin/main; then
@@ -93,7 +95,14 @@ jobs:
exit 0
fi
done < <(git for-each-ref --format='%(refname)' refs/remotes/origin/release)
echo "Plugin ClawHub publishes must target a commit reachable from main or release/*." >&2
if [[ "${WORKFLOW_REF}" =~ ^refs/heads/tideclaw/alpha/[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{4}Z$ ]]; then
alpha_branch="${WORKFLOW_REF#refs/heads/}"
git fetch --no-tags origin "+refs/heads/${alpha_branch}:refs/remotes/origin/${alpha_branch}"
if git merge-base --is-ancestor HEAD "refs/remotes/origin/${alpha_branch}"; then
exit 0
fi
fi
echo "Plugin ClawHub publishes must target a commit reachable from main, release/*, or the matching Tideclaw alpha branch." >&2
exit 1
- name: Validate publishable plugin metadata
@@ -168,6 +177,19 @@ jobs:
echo "::error::One or more selected plugin versions already exist on ClawHub. Bump the version before running a real publish."
exit 1
- name: Validate Tideclaw alpha plugin channels
if: startsWith(github.ref, 'refs/heads/tideclaw/alpha/')
run: |
set -euo pipefail
invalid="$(
jq -r '.candidates[]? | select(.publishTag != "alpha" or .channel != "alpha") | "- \(.packageName)@\(.version) [\(.publishTag)]"' .local/plugin-clawhub-release-plan.json
)"
if [[ -n "${invalid}" ]]; then
echo "Tideclaw alpha ClawHub publishes may only publish alpha plugin versions." >&2
printf '%s\n' "${invalid}" >&2
exit 1
fi
- name: Verify OpenClaw ClawHub package ownership
if: steps.plan.outputs.has_candidates == 'true'
env:

View File

@@ -25,7 +25,7 @@ on:
- selected
- all-publishable
ref:
description: Commit SHA on main or a release branch to publish from (copy from the preview run)
description: Commit SHA on main, a release branch, or the matching Tideclaw alpha branch to publish from
required: true
type: string
plugins:
@@ -71,7 +71,9 @@ jobs:
id: ref
run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
- name: Validate ref is on main or a release branch
- name: Validate ref is on a trusted publish branch
env:
WORKFLOW_REF: ${{ github.ref }}
run: |
set -euo pipefail
git fetch --no-tags origin \
@@ -85,7 +87,14 @@ jobs:
exit 0
fi
done < <(git for-each-ref --format='%(refname)' refs/remotes/origin/release)
echo "Plugin npm publishes must target a commit reachable from main or release/*." >&2
if [[ "${WORKFLOW_REF}" =~ ^refs/heads/tideclaw/alpha/[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{4}Z$ ]]; then
alpha_branch="${WORKFLOW_REF#refs/heads/}"
git fetch --no-tags origin "+refs/heads/${alpha_branch}:refs/remotes/origin/${alpha_branch}"
if git merge-base --is-ancestor HEAD "refs/remotes/origin/${alpha_branch}"; then
exit 0
fi
fi
echo "Plugin npm publishes must target a commit reachable from main, release/*, or the matching Tideclaw alpha branch." >&2
exit 1
- name: Validate publishable plugin metadata
@@ -151,6 +160,19 @@ jobs:
echo "Already published / skipped:"
jq -r '.skippedPublished[]? | "- \(.packageName)@\(.version)"' .local/plugin-npm-release-plan.json
- name: Validate Tideclaw alpha plugin channels
if: startsWith(github.ref, 'refs/heads/tideclaw/alpha/')
run: |
set -euo pipefail
invalid="$(
jq -r '.candidates[]? | select(.publishTag != "alpha" or .channel != "alpha") | "- \(.packageName)@\(.version) [\(.publishTag)]"' .local/plugin-npm-release-plan.json
)"
if [[ -n "${invalid}" ]]; then
echo "Tideclaw alpha plugin npm publishes may only publish alpha plugin versions." >&2
printf '%s\n' "${invalid}" >&2
exit 1
fi
preview_plugin_pack:
needs: preview_plugins_npm
if: needs.preview_plugins_npm.outputs.has_candidates == 'true'

View File

@@ -60,13 +60,17 @@ jobs:
authorize_actor:
name: Authorize workflow actor
runs-on: blacksmith-8vcpu-ubuntu-2404
outputs:
authorized: ${{ steps.permission.outputs.authorized }}
steps:
- name: Require maintainer-level repository access
id: permission
uses: actions/github-script@v8
with:
script: |
if (context.eventName === "schedule") {
core.info("Scheduled default-branch QA run; actor permission check is only required for manual dispatch.");
core.setOutput("authorized", "true");
return;
}
const allowed = new Set(["admin", "maintain", "write"]);
@@ -79,14 +83,18 @@ jobs:
const permission = data.permission;
core.info(`Actor ${context.actor} permission: ${permission}`);
if (!allowed.has(permission)) {
core.setFailed(
core.notice(
`Workflow requires write/maintain/admin access. Actor "${context.actor}" has "${permission}".`,
);
core.setOutput("authorized", "false");
return;
}
core.setOutput("authorized", "true");
validate_selected_ref:
name: Validate selected ref
needs: authorize_actor
if: needs.authorize_actor.outputs.authorized == 'true'
runs-on: blacksmith-8vcpu-ubuntu-2404
outputs:
selected_revision: ${{ steps.validate.outputs.selected_revision }}
@@ -178,6 +186,8 @@ jobs:
install-bun: "true"
- name: Build private QA runtime
env:
NODE_OPTIONS: --max-old-space-size=8192
run: pnpm build
- name: Run OpenAI candidate lane
@@ -188,7 +198,7 @@ jobs:
--concurrency "${QA_PARITY_CONCURRENCY}" \
--model "${OPENCLAW_CI_OPENAI_MODEL}" \
--alt-model openai/gpt-5.5-alt \
--output-dir .artifacts/qa-e2e/gpt54
--output-dir .artifacts/qa-e2e/openai-candidate
- name: Run Opus 4.7 lane
run: |
@@ -197,15 +207,15 @@ jobs:
--parity-pack agentic \
--concurrency "${QA_PARITY_CONCURRENCY}" \
--model anthropic/claude-opus-4-7 \
--alt-model anthropic/claude-sonnet-4-7 \
--output-dir .artifacts/qa-e2e/opus46
--alt-model anthropic/claude-sonnet-4-6 \
--output-dir .artifacts/qa-e2e/anthropic-baseline
- name: Generate parity report
run: |
pnpm openclaw qa parity-report \
--repo-root . \
--candidate-summary .artifacts/qa-e2e/gpt54/qa-suite-summary.json \
--baseline-summary .artifacts/qa-e2e/opus46/qa-suite-summary.json \
--candidate-summary .artifacts/qa-e2e/openai-candidate/qa-suite-summary.json \
--baseline-summary .artifacts/qa-e2e/anthropic-baseline/qa-suite-summary.json \
--candidate-label "${OPENCLAW_CI_OPENAI_MODEL}" \
--baseline-label anthropic/claude-opus-4-7 \
--output-dir .artifacts/qa-e2e/parity
@@ -219,6 +229,96 @@ jobs:
retention-days: 14
if-no-files-found: warn
run_live_runtime_token_efficiency:
name: Run live runtime token-efficiency lane
needs: [authorize_actor, validate_selected_ref]
if: github.event_name == 'schedule'
runs-on: blacksmith-8vcpu-ubuntu-2404
timeout-minutes: 45
environment: qa-live-shared
env:
QA_PARITY_CONCURRENCY: "1"
OPENCLAW_QA_TRANSPORT_READY_TIMEOUT_MS: "180000"
OPENCLAW_QA_REDACT_PUBLIC_METADATA: "1"
steps:
- name: Checkout selected ref
uses: actions/checkout@v6
with:
persist-credentials: false
ref: ${{ needs.validate_selected_ref.outputs.selected_revision }}
fetch-depth: 1
- 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 required QA credential env
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
shell: bash
run: |
set -euo pipefail
if [[ -z "${OPENAI_API_KEY:-}" ]]; then
echo "Missing required OPENAI_API_KEY." >&2
exit 1
fi
- name: Build private QA runtime
env:
NODE_OPTIONS: --max-old-space-size=8192
run: pnpm build
- name: Run live runtime parity lane
id: run_lane
shell: bash
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENCLAW_LIVE_OPENAI_KEY: ${{ secrets.OPENAI_API_KEY }}
run: |
set -euo pipefail
output_dir=".artifacts/qa-e2e/runtime-token-efficiency-live-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
echo "output_dir=${output_dir}" >> "$GITHUB_OUTPUT"
pnpm openclaw qa suite \
--repo-root . \
--provider-mode live-frontier \
--runtime-parity-tier standard \
--runtime-parity-tier live-only \
--concurrency "${QA_PARITY_CONCURRENCY}" \
--model "${OPENCLAW_CI_OPENAI_MODEL}" \
--alt-model "${OPENCLAW_CI_OPENAI_MODEL}" \
--runtime-pair pi,codex \
--fast \
--allow-failures \
--output-dir "${output_dir}/runtime-suite"
- name: Generate live runtime token-efficiency report
if: always() && steps.run_lane.outcome != 'skipped' && steps.run_lane.outcome != 'cancelled'
shell: bash
run: |
set -euo pipefail
pnpm openclaw qa parity-report \
--repo-root . \
--runtime-axis \
--token-efficiency \
--summary "${{ steps.run_lane.outputs.output_dir }}/runtime-suite/qa-suite-summary.json" \
--output-dir "${{ steps.run_lane.outputs.output_dir }}/runtime-report"
- name: Upload live runtime token-efficiency artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: qa-live-runtime-token-efficiency-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ steps.run_lane.outputs.output_dir }}
retention-days: 14
if-no-files-found: warn
run_live_matrix:
name: Run Matrix live QA lane
needs: [authorize_actor, validate_selected_ref]
@@ -254,6 +354,8 @@ jobs:
fi
- name: Build private QA runtime
env:
NODE_OPTIONS: --max-old-space-size=8192
run: pnpm build
- name: Run Matrix live lane
@@ -338,6 +440,8 @@ jobs:
fi
- name: Build private QA runtime
env:
NODE_OPTIONS: --max-old-space-size=8192
run: pnpm build
- name: Run Matrix live lane shard
@@ -420,6 +524,8 @@ jobs:
require_var OPENCLAW_QA_CONVEX_SECRET_CI
- name: Build private QA runtime
env:
NODE_OPTIONS: --max-old-space-size=8192
run: pnpm build
- name: Run Telegram live lane
@@ -513,6 +619,8 @@ jobs:
require_var OPENCLAW_QA_CONVEX_SECRET_CI
- name: Build private QA runtime
env:
NODE_OPTIONS: --max-old-space-size=8192
run: pnpm build
- name: Run Discord live lane
@@ -547,8 +655,8 @@ jobs:
--repo-root . \
--output-dir "${output_dir}" \
--provider-mode live-frontier \
--model openai/gpt-5.4 \
--alt-model openai/gpt-5.4 \
--model openai/gpt-5.5 \
--alt-model openai/gpt-5.5 \
--fast \
--credential-source convex \
--credential-role ci \
@@ -568,6 +676,9 @@ jobs:
needs: [authorize_actor, validate_selected_ref]
runs-on: blacksmith-8vcpu-ubuntu-2404
timeout-minutes: 60
concurrency:
group: qa-live-whatsapp-shared
cancel-in-progress: false
environment: qa-live-shared
steps:
- name: Checkout selected ref
@@ -606,6 +717,8 @@ jobs:
require_var OPENCLAW_QA_CONVEX_SECRET_CI
- name: Build private QA runtime
env:
NODE_OPTIONS: --max-old-space-size=8192
run: pnpm build
- name: Run WhatsApp live lane
@@ -699,6 +812,8 @@ jobs:
require_var OPENCLAW_QA_CONVEX_SECRET_CI
- name: Build private QA runtime
env:
NODE_OPTIONS: --max-old-space-size=8192
run: pnpm build
- name: Run Slack live lane

View File

@@ -2,8 +2,12 @@ name: Workflow Sanity
on:
pull_request:
paths-ignore:
- "CHANGELOG.md"
push:
branches: [main]
paths-ignore:
- "CHANGELOG.md"
workflow_dispatch:
permissions:

View File

@@ -35,14 +35,18 @@ Skills own workflows; root owns hard policy and routing.
- External official plugins own package/deps and are excluded from core dist; core uses registry-aware `facade-runtime` or generic contracts.
- Externalizing a bundled plugin: update package excludes, official catalogs, docs, tests, and prove core runtime paths resolve installed plugin roots before root-dep removal.
- Legacy config repair belongs in `openclaw doctor --fix`, not startup/load-time core migrations. Runtime paths use canonical contracts.
- New seams: backward-compatible, documented, versioned. Third-party plugins exist.
- Fix shape: prefer bounded owner-boundary refactors over local patches/shims when they remove stale abstractions, duplicate policy, or wrong ownership.
- Compat default: no new internal shims, aliases, fallback APIs, or legacy names just to reduce diff. Migrate callers and delete old paths.
- Public plugin API is the only compat exception: document/version breaks, aggressively deprecate unused SDK surface, and migrate ALL bundled/internal plugins to the modern API in the same change.
- 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.
- 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.
- Config contract: exported types, schema/help, metadata, baselines, docs aligned. Retired public keys stay retired; compat in raw migration/doctor only.
- Prompt cache: deterministic ordering for maps/sets/registries/plugin lists/files/network results before model/tool payloads. Preserve old transcript bytes when possible.
- Agent tool schema cleanup: remove stale args cleanly; no hidden compat for model-facing params just to avoid churn.
## Commands
@@ -63,12 +67,13 @@ Skills own workflows; root owns hard policy and routing.
## Validation
- Use `$openclaw-testing` for test/CI choice and `$crabbox` for remote/full/E2E proof.
- Crabbox request means real scenario proof: install/update/call/repro user path; not just copy tests and run them remotely.
- Small/narrow tests, lints, format checks, and type probes are fine locally only in a healthy normal checkout.
- In Codex worktrees, direct local `pnpm test*`, `pnpm check*`, `pnpm crabbox:run`, and `scripts/committer` can trigger pnpm dependency reconciliation or install prompts. Prefer `node` wrappers locally and Crabbox/Testbox for pnpm-gated proof.
- Full suites, broad changed gates, Docker/package/E2E/live/cross-OS proof, or anything that bogs down the Mac: Crabbox/Testbox.
- One/few files local. If a local command fans out, stop and move broad proof to Crabbox/Testbox.
- Before handoff/push: prove touched surface. Before landing to `main`: issue proof plus appropriate full/broad proof unless scope is clearly narrow.
- Pre-land/pre-commit code changes: use `$codex-review` until no accepted/actionable findings remain, unless equivalent manual review already done, trivial/docs-only, or user opts out.
- Pre-land/pre-commit code changes: use `$autoreview` until no accepted/actionable findings remain, unless equivalent manual review already done, trivial/docs-only, or user opts out.
- If proof is blocked, say exactly what is missing and why.
- Do not land related failing format/lint/type/build/tests. If unrelated on latest `origin/main`, say so with scoped proof.
- Docs/changelog-only and CI/workflow metadata-only: `git diff --check` plus relevant docs/workflow sanity; escalate only if scripts/config/generated/package/runtime behavior changed.

View File

@@ -1,4 +1,4 @@
# Changelog
# Changelog
Docs: https://docs.openclaw.ai
@@ -6,42 +6,410 @@ Docs: https://docs.openclaw.ai
### Changes
- 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.
- Skills: rename the repo-local Codex closeout review skill and helper to `autoreview` while preserving the Codex-first fallback behavior.
- Skills: add a meme-maker skill for curated template search, local SVG/PNG rendering, Imgflip hosted rendering, and Know Your Meme provenance links.
- Browser: surface pending and recently handled modal dialogs in snapshots, return `blockedByDialog` when an action opens a modal, and allow `browser dialog --dialog-id` to answer pending dialogs.
- 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.
- Skills: add node inspector debugging, fused diagram generation, and throwaway spike workflow skills.
- CLI/plugins: add `defineToolPlugin` plus `openclaw plugins build`, `validate`, and `init` for typed simple tool plugins with generated manifest metadata, optional tool declarations, and context factories.
- Agents/skills: tighten bundled skill prompts and metadata, quote skill descriptions, refresh current CLI/API guidance, and update embedded sherpa-onnx runtime downloads.
- Skills: update the Obsidian skill to target the official `obsidian` CLI and require its registered binary instead of the third-party `obsidian-cli`.
- Skills: add a Python debugging skill for pdb, breakpoint(), post-mortem inspection, and debugpy remote attach.
- Plugins/messages: add presentation capability limits for channel renderers, adapt rich message controls before native rendering, and mark legacy `interactive`/Slack directive producer APIs as deprecated.
- Proxy: support HTTPS managed forward-proxy endpoints and scoped `proxy.tls.caFile` CA trust for proxy endpoint TLS. (#79171) Thanks @jesse-merhi.
- 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.
- QA-Lab: add `openclaw qa suite --runtime-parity-tier` and wire the standard Codex-vs-Pi tier into release checks separately from optional/live-only/soak lanes. Fixes #80337. Thanks @100yenadmin.
- 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.
- 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.
- 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.
- QA-Lab: expose runtime tool fixture coverage through `openclaw qa coverage --tools`, with optional suite-summary evaluation for parity gate artifacts. Thanks @100yenadmin.
- QA-Lab: schedule a live-frontier Codex-vs-Pi runtime token-efficiency artifact lane in the all-lanes QA workflow. Fixes #80175. Thanks @100yenadmin.
- 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.
- 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.
### Fixes
- Gateway/skills: preflight remote macOS skill-bin refreshes with a WebSocket connectivity check so stale node sessions skip quickly instead of logging slow `system.which` timeout warnings.
- GitHub Copilot: drop unsafe native Responses reasoning replay items with non-replayable IDs before dispatch, preventing affected Copilot sessions from failing with `invalid_request_body`. Fixes #83220. Thanks @galiniliev.
- QA-Lab: make runtime tool coverage fail on missing required tool exercise instead of treating pass/pass parity envelope drift as missing coverage.
- 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.
- 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.
- 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.
- Mac app: keep app-level menu commands and Dashboard failure states reachable when the remote Gateway is disconnected, and keep the Settings sidebar toggle in the leading titlebar area.
- Mac app: allow longer Gateway and Context errors to wrap in the menu instead of truncating the useful failure detail.
- Gateway/webchat: hide internal runtime-context and other `display: false` transcript messages from Chat history and live message events. Fixes #83216. Thanks @EmpireCreator.
- CLI/help: keep `gateway`, `doctor`, `status`, and `health` help registration out of action/runtime imports so subcommand `--help` stays lightweight in constrained terminals. Fixes #83228. Thanks @dfguerrerom.
- 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.
- 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.
- 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.
- Telegram: fail topic sends closed when Telegram reports `message thread not found` instead of retrying without `message_thread_id` into the base chat. Refs #83302.
- Mac app: align the Sessions settings pane with the standard Settings page gutter and row spacing.
- OpenAI/Codex: stop rejecting available `openai-codex` GPT-5.1, GPT-5.2, and GPT-5.3 model refs during config validation, while keeping removed Spark aliases suppressed. Fixes #83303.
- 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.
- Codex app-server: preserve streamed native command output in mirrored transcripts and trajectory exports when final snapshots omit aggregated output. (#83200) Thanks @rozmiarD.
- 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.
- 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.
- 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.
- 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.
- Plugins: apply a default 15-second timeout to legacy `before_agent_start` hooks so hung plugin handlers no longer block agent startup. Fixes #48534. (#83136) Thanks @therahul-yo.
- Feishu: refresh inbound session delivery context for DM, group, and broadcast turns so later replies do not inherit stale WebChat routing. Fixes #78274.
- 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.
- QA-Lab/qa-channel: attach redacted agent tool-start traces to outbound `QaBusMessage` records so scenarios can assert actual tool use instead of relying only on reply text. Fixes #67637. Thanks @100yenadmin.
- QA-Lab: fail live runtime parity reports when assistant-message usage is missing, preventing `0 vs 0` live token rows from being reported as passing proof. Fixes #80411. Thanks @100yenadmin.
- 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.
- 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.
- 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.
- 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.
- 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.
- Mac app: avoid a SwiftUI metadata crash when rendering the Cron Jobs settings pane.
- 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.
- Agents/OpenAI streams: yield via `setTimeout(0)` instead of `setImmediate` between bursty Responses chunks so abort timers can fire during the yield, keeping cancel-on-timeout responsive on hot streams. Refs #82462.
- Agents/Codex: keep legacy `oauthRef`-backed OAuth profiles usable while `openclaw doctor --fix` migrates them back to inline credentials, without creating new sidecar credentials. (#83312) Thanks @joshavant.
- CLI/config: send SecretRef diagnostics to stderr so JSON command stdout remains parseable.
- CLI/doctor: seed Control UI allowed origins when migrating legacy non-loopback gateway bind host aliases like `0.0.0.0`. Fixes #83286. Thanks @giodl73-repo.
- CLI/plugins: ship the bundled memory CLI as a package entry so package-installed `openclaw memory` commands register correctly.
- 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.
- Feishu: detect SecretRef top-level credentials as a configured default account instead of treating object-backed app secrets as missing.
- 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.
- CLI/completion: resolve concrete PowerShell profile paths and reload commands during setup and doctor completion installation. Fixes #44296. (#83059) Thanks @yu-xin-c.
- Telegram: keep isolated long polling below the hard `getUpdates` request guard so idle bot accounts with high `timeoutSeconds` do not false-disconnect and restart-loop. Fixes #83264. Thanks @riccodecarvalho.
- Providers/Google: preserve and recover Gemini 3 tool-call thought signatures during native replay so function-calling turns no longer fail with missing `thought_signature` 400s. Fixes #72879. (#80358) Thanks @abnershang.
- 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.
- 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.
- Memory-core: distinguish sqlite-vec load failures from missing semantic vector embeddings in degraded `memory index` 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- QA-Lab: clean orphaned gateway temp roots when a suite parent exits and wait on gateway plus transport readiness after config restarts, reducing stale `qa-channel` noise from interrupted runs. Fixes #65506. Thanks @100yenadmin.
- 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.
- 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.
- 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.
- Config/models: accept `thinkingFormat: "together"` in model compat config so Together routes can opt into the Together-specific thinking response shape.
- 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.
- 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.
- Mac app: refine the Settings General and Connection panes with cleaner status panels, card rows, and a single native titlebar sidebar toggle.
- 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.
- Agents/music: steer song, jingle, beat, anthem, and instrumental requests toward `music_generate` audio creation instead of lyric-only replies, and reserve `lyrics` for exact sung words.
- 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.
- 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)
- 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.
- 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)
- 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.
- CLI/sessions: accept `openclaw sessions list` as an alias for `openclaw sessions`, matching other list-style commands. Fixes #81139. (#81163) Thanks @YB0y.
- Channels/stream previews: widen compact progress draft lines and cut prose at word boundaries while preserving command/path suffixes, with `streaming.progress.maxLineChars` for channel-specific tuning.
- CLI/plugins: have `openclaw plugins doctor` warn when a configured runtime needs a missing owner plugin, sharing the same install mapping as `openclaw doctor --fix`. Fixes #81326. (#81674) Thanks @Zavianx.
- Agents/Codex: route OpenAI runs that resolve to `openai-codex` through the Codex provider and bootstrap OpenClaw's stored OAuth profile into the Codex harness when the harness owns transport, so `openai/*` model refs no longer fail with `No API key found for openai-codex` despite an existing Codex OAuth profile. (#82864) Thanks @ragesaq.
- 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.
- Agents/memory: explain that memory-triggered compaction exposes only `read` and append-only `write` when configured core tools are unavailable in `tools.allow` warnings. Fixes #82941. Thanks @galiniliev.
- Agents/OpenAI: preserve deterministic tool payload ordering for prompt-cache reuse across OpenAI Responses and chat completions calls. (#82940) Thanks @galiniliev.
- 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.
- Telegram: warn when a media group drops photos that fail to download, including albums where every photo is skipped. Fixes #55216. (#82987) Thanks @eldar702.
- Agents/skills: apply the full effective tool policy pipeline to inline `command-dispatch: tool` skill dispatch before owner-only filtering, preserving configured allow, deny, sandbox, sender, group, and subagent restrictions. (#78525)
- 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.
- 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 `default` instead of silently dropping it.
- Codex/Telegram: synthesize native Codex tool progress from final turn snapshots so Telegram `/verbose` stays visible when command events arrive only at completion.
- Codex/Telegram: deliver Codex verbose tool summaries in direct message-tool-only turns while suppressing message-send and activity-log noise. (#83186) Thanks @kurplunkin.
- 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.
- 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.
- Gateway/protocol: restore Gateway WS protocol v4 and keep `message.action` room-event metadata on the existing `inboundTurnKind` wire field while preserving internal inbound-event classification.
- 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.
- Channels: keep direct-message last-route writes on isolated `per-channel-peer` sessions instead of contaminating the agent main session with channel delivery context. Fixes #36614. Thanks @aspenas.
- Mac app: move the Settings sidebar toggle into the native titlebar and tighten the General pane width.
- Mac app: keep visited Settings panes mounted so switching tabs no longer blanks and reloads their content.
- 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.
- 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.
- 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.
- 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)
- Providers/Together: update PI runtime packages to 0.74.1 and emit Together-style `reasoning.enabled`/`max_tokens` controls for reasoning-capable OpenAI-completions models.
- Agents/diagnostics: split slow embedded-run `attempt-dispatch` startup summaries into workspace, prompt, runtime-plan, and final dispatch subspans so traces identify the delayed setup phase. Fixes #82782. (#82783) Thanks @galiniliev.
- Agents/Codex: flatten nested tool-result middleware blocks into bounded text so successful message sends are no longer replaced with `Tool output unavailable due to post-processing error`. Fixes #82912. Thanks @joeykrug.
- CLI/media: accept HTTP(S) URLs in `openclaw infer image describe --file`, fetching remote images through the guarded media path instead of treating URLs as local files. Fixes #82837. (#82854) Thanks @neeravmakwana.
- 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.
- Control UI: advertise shared Gateway protocol constants in browser connect frames, fixing protocol mismatch handshakes after protocol constant drift. Fixes #82882. Thanks @galiniliev.
- 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)
- 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.
- Gateway/auth: allow same-host trusted-proxy callers to use the documented local direct `gateway.auth.password` 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.
- 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.
- 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.
- 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.
- 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.
- CLI/models: reuse command-scoped plugin metadata across model listing, provider catalog, auth, and synthetic-auth checks, restoring fast `openclaw models` runs for plugin-heavy installs. Fixes #82881. (#83033) Thanks @joshavant.
- CLI/channels: show configured official external channels such as Discord in `openclaw channels list` when their plugin package is missing, including the install and doctor repair command instead of reporting no configured channels. Fixes #82813.
- Signal: preserve mixed-case group IDs through routing and session persistence so group auto-replies keep delivering after updates. Fixes #82827.
- Agents/tools: keep the `message` tool available in embedded runs when it is explicitly allowed through `tools.alsoAllow` or runtime tool allowlists, so channel plugins with custom reply delivery can still use configured message sends. Fixes #82833. Thanks @cn1313113.
- WhatsApp: honor forced document delivery for outbound image, GIF, and video media so `forceDocument`/`asDocument` sends preserve original media bytes instead of using compressed media payloads. (#79272) Thanks @itsuzef.
- WhatsApp: name outbound document attachments from their MIME type when no filename is provided, so PDF and CSV sends arrive as `file.pdf` and `file.csv` instead of an extensionless `file`. Thanks @mcaxtr.
- Process/diagnostics: report active lane blockers in lane wait warnings so `queueAhead=0` no longer hides commands waiting behind active work. Fixes #82791. (#82792) Thanks @galiniliev.
- 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.
- 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.
- 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.
- 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.
- CLI/status: render extra gateway-like service diagnostics as warning/info output instead of error output. Fixes #46930. (#82922) thanks @giodl73-repo.
- 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.
- Plugin SDK: bundle `openclaw/plugin-sdk/zod` 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 `zod` symlink. Fixes #78398. (#78515) Thanks @ggzeng.
- Providers/Google: drop compaction-truncated Gemini thought signatures before replay so malformed Base64 no longer aborts the next assistant turn. (#82995) Thanks @wAngByg.
## 2026.5.17
### Changes
- Control UI: move settings-only destinations into the Settings workspace and add sidebar recent-session shortcuts plus a one-click new-session action.
- Control UI: speed up scoped settings pages by loading required config before schema refreshes, caching burst schema responses, and opening Communications on lighter message settings first.
- Control UI: simplify the Cron Jobs workspace with modal job creation, collapsed filters, and an empty state aimed at first-time setup.
- Security/audit: add `security.audit.suppressions` for intentionally accepted audit findings, keeping suppressed matches out of the active summary while preserving them in JSON output with an active suppression notice. (#76949) Thanks @100menotu001.
- Agents/subagents: label delegated task and subagent completion handoffs as ready for parent review, and tell requester agents to review/verify results before calling them done. (#78985) Thanks @100menotu001.
- Providers/media: add fal and OpenRouter music-generation providers for the shared `music_generate` tool, including fal MiniMax/ACE/Stable Audio endpoints and OpenRouter Lyria audio output.
- Maintainer tooling: warn before running JS package commands on raw Crabbox AWS boxes, pointing maintainers to Actions hydration or Blacksmith Testbox for CI-like proof.
- Control UI: show provider quota usage in the Overview card and Chat header, and recover stale Chat in-progress state after missed terminal events. (#82647)
- Mac app remote setup can now be preconfigured from `openclaw-mac configure-remote`, skips onboarding when config is already complete, supports direct LAN/Tailnet gateway URLs, allows private same-origin Control UI loads, and owns the SSH tunnel process when SSH is selected.
- Providers/xAI: add xAI Grok OAuth login for SuperGrok subscribers, letting `xai/*` models and xAI media/tool providers authenticate without `XAI_API_KEY`.
- CLI/cron: add `openclaw cron run --wait` with timeout and poll interval controls, plus exact `cron.runs --run-id` filtering so automation can block on one queued manual run. (#81929) Thanks @ificator.
- Maintainer tooling: route Crabbox skill defaults through the repo brokered AWS config, leaving Blacksmith Testbox as an explicit opt-in instead of the broad-proof default.
- CLI/onboarding: localize the setup wizard and bundled channel setup flows for English, Simplified Chinese, and Traditional Chinese. (#80645) Thanks @GaosCode.
- Agents/skills: cache hydrated `resolvedSkills` across warm gateway turns while keying reuse by the redacted effective config, reducing redundant skill snapshot rebuilds without crossing config-gated skill boundaries. (#81451) Thanks @solodmd.
- Telegram/group chat: add opt-in `messages.groupChat.ambientTurns: "room_event"` handling so always-on ambient chatter can run as quiet room context and speak visibly only via the message tool. (#81317) Thanks @obviyus.
- Group chat: add core inbound event classification with opt-in `messages.groupChat.unmentionedInbound: "room_event"`, so always-on unmentioned room chatter can run as quiet context and speak visibly only via the message tool. (#81317) Thanks @obviyus.
- Codex/context engines: bind thread-bootstrap projection epochs to Codex app-server threads, carry redacted tool-result context into fresh threads, and rotate backend threads when projection state changes. (#82351) Thanks @jalehman.
- Agents/media: run `image_generate` through the shared async media-generation task lifecycle in session-backed chats, with task status, duplicate guarding, and message-tool completion delivery matching music/video.
- Gateway: add opt-in restart trace logs for restart signal, active-work drain, close, next-start, ready, and memory spans. (#82396) Thanks @samzong.
- Gateway/performance: split startup benchmark HTTP-listen timing from full gateway-ready timing and add post-bind plugin and sidecar diagnostics to restart-readiness traces. (#82603) Thanks @samzong.
- QA-Lab: add a deterministic local personal-agent scenario pack covering reminders, threaded replies, scoped memory recall, redaction, and safe tool followthrough. (#78219) Thanks @iFiras-Max1.
- QA-Lab: add `--pack personal-agent` for `openclaw qa suite` so maintainers can run the accepted personal-agent scenario pack by selector. (#82760) Thanks @iFiras-Max1.
- QA-Lab: add a private Codex-vs-Pi runtime parity axis with runtime-pair suite runs, parity reports, and release-check wiring. (#80238) Thanks @100yenadmin.
- Slack: add Slack assistant thread lifecycle support with assistant view manifest entries, suggested prompts, thread-scoped assistant sessions, and Slack-provided assistant context. Fixes #80787. Thanks @mobybot27.
### Fixes
- Codex/app-server: cover `/btw` side-question native hooks and app-server command approvals without relying on unsupported turn-scoped hook config. (#82559) Thanks @Kaspre.
- Gateway/Docker: fail closed for non-loopback gateway starts without explicit shared-secret or trusted-proxy auth, and stop the image default command from bypassing config validation. Fixes #82865. (#82866) Thanks @coygeek.
- Agents/followups: route queued followup turns through CLI runtime backends instead of embedded harness lookup, preventing `claude-cli`/`google-gemini-cli` followups from failing before delivery. Fixes #82847. (#82857) Thanks @hclsys.
- CLI/sessions: let `openclaw sessions cleanup --fix-missing` prune malformed rows with unresolvable transcript metadata instead of throwing. Fixes #80970. (#82745) Thanks @IWhatsskill.
- Gateway/usage: refresh large session usage summaries in the background and reuse durable transcript metadata so `sessions.usage` no longer blocks Gateway requests on full transcript rescans. Fixes #82773. (#82778) Thanks @hclsys.
- CLI/MiniMax media: let `openclaw infer image describe --file` accept HTTP(S) image URLs without treating them as local paths, and keep automatic MiniMax image understanding routed through `MiniMax-VL-01` even when legacy MiniMax M2.x chat metadata claims image input. Fixes #82837. Thanks @mGaolin.
- TUI: restore the submitted draft when chat is busy instead of clearing it or queueing another run. Fixes #45326. (#82774) Thanks @hyspacex.
- Cron/memory: treat claimed `before_agent_reply` cron hooks as execution progress, so long memory dreaming promotion jobs are not aborted by the isolated-run pre-execution watchdog. Fixes #82811.
- Discord: recover transcript-backed full answers when progress-mode final payloads are ellipsis-truncated, so long replies fall back to normal chunked delivery instead of replacing the preview with a shortened message. Fixes #82807. Thanks @blueberry6401.
- Browser plugin: redact attach-details from Chrome MCP diagnostics and keep raw Chrome launch error output around long enough to surface in user reports without leaking sensitive paths.
- System prompts: clarify MEMORY guidance over generic TTS hints in the embedded speech-core/system-prompt scaffolding so agents prefer memory-store usage over speech defaults. Fixes #81930. Thanks @giodl73-repo.
- Agents/auth: include the checked credential source in missing API key errors, so users can see which env var, profile, or config path to fix. Fixes #82785. Thanks @loeclos.
- Providers/GitHub Copilot: hash Responses replay item ids with sha256 instead of a weak 32-bit hash and build same-provider Copilot tool-call ids distinctly, so concurrent tool-call replays no longer collide and reject follow-up turns.
- Agents/replay: normalize malformed assistant replay content before transport conversion while preserving empty-stop replay repair, so bad provider history no longer crashes with non-iterable content. Fixes #43795. (#82748) Thanks @IWhatsskill.
- Gateway/macOS: write LaunchAgent stdout under `~/Library/Logs/openclaw`, suppress stderr, and attach stdin to `/dev/null` so launchd startup avoids symlinked state-dir log failures and silent module-evaluation hangs. Fixes #40207 and #46153. Thanks @dhruvkelawala and @frankr.
- CLI/configure: let model-only section setup enter provider auth directly instead of first asking where the Gateway runs, unblocking OAuth/token setup in terminals where that unrelated prompt is unresponsive. Fixes #39223. Thanks @LevityLeads.
- Providers/Anthropic-messages: extract `reasoning_content` from `thinking` blocks during assistant replay so proxy providers that route through the Anthropic-messages transport preserve reasoning context across tool-call follow-up turns. Thanks @Sunnyone2three.
- Agents/GitHub Copilot: normalize replayed Responses tool-call IDs before dispatch so resumed sessions with historical overlong tool IDs continue instead of failing Copilot schema validation. (#82750) Thanks @galiniliev.
- CLI/infer: resolve plugin-scoped web search and fetch SecretRefs on the exact command credential surface, keeping non-selected and unrelated plugin secrets inactive. Fixes #82621. (#82699) Thanks @leno23.
- Providers/Anthropic Vertex: resolve installed provider public surfaces from package-local `dist/`, restoring `anthropic-vertex/*` model calls after plugin externalization. Fixes #82781. Thanks @0L1v3DaD.
- Gateway/exec approvals: bind path-shaped allowlists, safe-bin trust, skill auto-allow, Allow Always persistence, and approval audit metadata to the executable realpath so symlinked binaries cannot keep approvals after retargeting. Fixes #45595. Thanks @jasonftl.
- Mac app: reorganize Settings around a grouped sidebar, with separate Connection and Exec Approvals pages so everyday permissions and app toggles are easier to scan.
- Mac app: redraw the animated menu bar critter to match the rounded app mascot with antennae, side arms, two feet, and smoother template rendering.
- Mac app: cache settings config schema/drafts and load channel config in parallel with channel probes, making repeated Channels and Config tab switches responsive over remote tunnels.
- Control UI: negotiate the Gateway protocol from shared constants so rebuilt dashboards connect to current gateways instead of reporting a protocol mismatch.
- Mac app: let menu gateway/session error text wrap across a few lines and stop rebuilding dynamic Context/Gateway menu rows while the menu is open, reducing flicker.
- QA-Lab: expose Codex runtime tools during private parity runs and treat completed structural/tool-shape runtime drift as advisory, while preserving real runtime failures as lane blockers.
- Mac app: make device pairing approval sheets friendlier, with concise Mac/device copy, shortened identifiers, friendly scope labels, and Approve as the primary action.
- Providers/Qwen: honor session thinking level for `qwen-chat-template` payloads so `/think off` disables nested llama.cpp chat-template thinking controls. Fixes #82768. Thanks @bfox55.
- Feishu/wiki: reject numeric wiki space IDs before creating Lark clients and keep numeric-looking IDs documented as quoted opaque strings, preventing JavaScript precision loss in knowledge base calls. Fixes #45301. (#82769) Thanks @hyspacex.
- Control UI: simplify Talk settings to Voice, Model, and Sensitivity defaults, with provider, transport, exact VAD, and timing controls behind Advanced.
- Telegram: let catch-all mention patterns match captionless group photos, so media-only group messages reach the agent when the group is intentionally configured to respond to all messages. Fixes #44833. (#82756) Thanks @IWhatsskill.
- Gateway/pairing: reject forged loopback Control UI origins from non-local proxy paths, and keep mobile pairing setup on Tailscale bind mode pointing users to Tailscale Serve/Funnel instead of cleartext tailnet WebSockets.
- Telegram/Gateway: persist isolated polling offsets only after main-thread dispatch and preserve gateway caller scopes for Telegram message actions, fixing consumed-but-unrouted polling updates and recursive CLI send scope approvals. Fixes #82277. (#82705) Thanks @udaymanish6.
- Memory-core: abort timed-out embedding provider calls so remote embedding HTTP requests do not continue running after memory query or indexing timeouts. Fixes #82732. Thanks @adityarya24.
- Channels/stream previews: contain rejected background draft-stream flushes so preview send failures do not surface as fatal unhandled rejections. Fixes #82712. (#82713) Thanks @coygeek.
- Codex/app-server: keep shared native app-server clients isolated per agent runtime key so starting one agent no longer closes another agent's active Codex turn. Fixes #82758. Thanks @PashaGanson.
- Providers/OpenAI Codex: include base `gpt-5.5` and `gpt-5.4` reasoning metadata in the bundled Codex catalog so `/think xhigh` remains available for those models. Fixes #82744.
- Providers/OpenAI Codex: keep the native hook relay as the final Codex app-server thread config patch so hook-backed approvals stay enabled even when lower-priority config disables hooks. Thanks @solomonneas.
- Providers/MiniMax: declare CN endpoint auth aliases in the plugin manifest so `minimax-cn` and `minimax-portal-cn` reuse the correct base auth profiles instead of falling back to unrelated models after 401s. Fixes #63823. Thanks @kamusis.
- Secrets/audit: treat `$VAR` auth-profile values as env SecretRefs and stop reporting env-ref credentials as plaintext, including mixed `keyRef` plus env-ref profile states. Fixes #53998. Thanks @schirloc and @artwalker.
- Agents/model fallback: suppress fallback notices when the active OpenAI Codex runtime reports the same canonical OpenAI model.
- Agents/music generation: remove model-controlled request timeouts, default internal provider requests to five minutes, and keep configured timeouts at a 120-second floor.
- Cron: let isolated best-effort deliveries send the parent result immediately while fire-and-forget subagents keep running, avoiding false run timeouts. Fixes #44428. Thanks @amknight.
- Agents/media generation: stop logging delivered failure summaries as missing message-tool delivery when no generated media was expected.
- Agents/sessions: prioritize manual user turns ahead of queued cron and maintenance work in the same session lane, so visible follow-ups no longer wait behind background runs. Fixes #82764. (#82765) Thanks @galiniliev.
- Agents/edit tool: honor `file_path` and related path aliases when resolving edit-recovery targets, so post-write errors no longer surface false edit failures after the file actually changed. Fixes #81909. Thanks @giodl73-repo.
- QQBot: treat only explicit truthy `QQBOT_DEBUG` values as enabling debug logs, so false-like values such as `0` no longer expose debug output. Fixes #82644. (#82697) Thanks @leno23.
- Agents/session_status: resolve implicit no-arg status lookups against the live run session, so `/think` changes report the current thinking level instead of stale sandbox state. Fixes #82669. (#82696) Thanks @leno23.
- Discord: keep progress drafts visible for message-tool-only guild replies under the default coding tool profile. Fixes #82747. Thanks @eliranwong.
- Agents: prefer current structured assistant final answers when assembling final reply payloads, reducing reliance on streamed preview fragments after channel transcript recovery. (#82850) Thanks @joshavant.
- Discord: keep unmentioned room-event history until a visible Discord send succeeds, so quiet ambient context does not disappear before message-tool delivery. (#82573) Thanks @obviyus.
- CLI/setup: order the model/auth provider picker as OpenAI, Anthropic, xAI, Google, then the remaining providers alphabetically.
- Diagnostics/usage/voice-call: treat explicit zero and non-finite limits as empty results and reject invalid voice-call numeric CLI flags. Fixes #82646, #82650, #82651, and #82653. (#82679) Thanks @leno23.
- CLI/config: avoid redundant startup config/plugin checks for the guided `openclaw config` flow and show progress while source checkout CLI artifacts build or load.
- Config/Mac app: accept `gateway.remote.remotePort` in core config validation so Mac SSH remote setup stays compatible with the CLI.
- Gateway/diagnostics: add opt-in critical memory pressure stability snapshots with gateway logs, V8 heap, cgroup, active-resource, and redacted large session-file evidence. Fixes #82518.
- Doctor/Gateway: avoid treating unrelated macOS LaunchAgents as legacy gateways just because their environment values mention old checkout paths.
- Gateway/heartbeat: defer heartbeat runs while the target reply operation is queued or active, preventing heartbeat prompts from interleaving with WebChat responses before the streaming lane starts. Fixes #82722. Thanks @Andy-Xie-1145.
- CLI/setup: collapse raw gateway config keys in existing-config summaries into friendly `Model` and `Gateway` rows.
- CLI/config: show concise human config-write output with an indented backup path instead of printing checksum-heavy overwrite audit details by default.
- Skills/onboarding: hide brew-only dependency installers in Linux containers without Homebrew and show container-specific guidance instead of a broken install path. Fixes #14593. Thanks @amknight.
- CLI/docs: call the canonical lowercase docs MCP search tool and surface MCP errors instead of returning empty search results. Fixes #82702. (#82704) Thanks @hclsys.
- QA-Lab: add gateway log sentinels for plugin hook failures, Codex app-server stalls/timeouts, cron allowlist drift, live quota blockers, and direct-reply self-message transcripts so harness proof fails on self-health regressions. (#80323) Thanks @100yenadmin.
- QA-Lab: ignore heartbeat-only operational transcripts when capturing runtime parity cells so background checks cannot replace the scenario reply. (#80323) Thanks @100yenadmin.
- QA-Lab: pin threaded-memory parity runs to `memory-core`, keep bundled plugin resolution enabled for QA commands, and retry transient session-store lock reads. (#72045) Thanks @WuKongAI-CMU.
- QA-Lab/qa-channel: keep mock memory ranking, inbound media notes, and opened-file realpath checks stable for mock OpenAI qa-channel runs. (#66826) Thanks @gumadeiras.
- Gateway/exec approvals: wait for accepted async approval follow-up runs instead of direct-fallback sending duplicate completions when retries use different nonce keys. Fixes #82711. (#82717) Thanks @udaymanish6.
- Agents/subagents: mark completed subagent handoffs as ready for parent review so requester agents verify results and continue required follow-up work before reporting done. (#82724) Thanks @100menotu001.
- QA-Lab: validate Capture saved views loaded from browser storage so malformed local state cannot poison Capture inspector filters or layout controls. (#77722) Thanks @AsaZhou923.
- Agents/performance: reuse prepared plugin manifest metadata across local CLI turns, model catalog normalization, auth lookups, and tool capability checks, restoring fast pre-provider startup for plugin-heavy installs. Thanks @shakkernerd.
- CLI/config: add `--dry-run` support to `openclaw config unset`, with `--json` output and allow-exec validation parity with `config set`/`config patch` dry-run handling. (#81895) Thanks @giodl73-repo.
- CLI/infer: resolve command SecretRefs before local provider-backed capability runs, so web search/fetch and other local infer commands can use plugin-scoped credential refs. Fixes #82621. (#82798) Thanks @joshavant.
- Memory-core: retry disabled dreaming cron cleanup until cron is available after startup, so persisted managed dreaming jobs are removed after restart. Fixes #82383. (#82389) Thanks @neeravmakwana.
- Providers/xAI: keep retired Grok 3, Grok 4 Fast, Grok 4.1 Fast, and Grok Code slugs out of model pickers while preserving compatibility resolution for existing configs.
- Providers/xAI: replace the retired `grok-imagine-image-pro` image model with `grok-imagine-image-quality` in the bundled image-generation provider and docs. (#81399) Thanks @KateWilkins.
- Providers/OAuth: let browser-hosted identity provider pages read successful localhost callback responses, preventing xAI Grok OAuth from showing a false connection failure after OpenClaw completes login.
- Gateway/security: reject malformed HTTP and WebSocket request targets with the existing auth failure response instead of letting invalid URL parsing crash the Gateway. Fixes GHSA-6hc3-f4rg-377m.
- Browser/CDP: redact credential-bearing Chrome MCP and managed Chrome launch diagnostics, and require exact loopback entries before treating `NO_PROXY` as already covering local CDP proxy bypasses.
- Gateway/auth: reuse prepared startup auth SecretRef snapshots when the gateway startup config is unchanged, avoiding duplicate runtime secret preparation. (#82991) Thanks @samzong.
- Gateway/diagnostics: redact credential-bearing gateway target URLs and client diagnostics while preserving raw connection URLs for programmatic use, so connect-failure logs no longer surface embedded tokens.
- Gateway/auth: honor `OPENCLAW_GATEWAY_TOKEN` as the remote interactive fallback when no remote token is configured, keeping remote TUI setup aligned with documented auth precedence.
- Providers/xAI: continue polling video generations while xAI reports in-flight jobs as `pending`, so Grok video requests no longer fail before the final `done` response. (#82610) Thanks @Manzojunior.
- Logs: redact raw Basic auth and named security headers from `logs.tail` output before returning lines to read-scoped clients. Fixes #66832. Thanks @Magicray1217.
- CLI/gateway: emit structured JSON for gateway transport close/timeout failures when `--json` is requested by health, gateway health, and devices list commands. Fixes #79108. Thanks @TurboTheTurtle.
- Agents/Telegram: retry Bedrock non-visible terminal turns and mark non-deliverable attempts as trajectory errors instead of silent success. Fixes #82394. (#82905) Thanks @joshavant.
- Telegram: normalize announce group targets via a new `resolveSessionTarget` channel hook so scheduled announcements resolve consistently against the same Telegram session conversation registry as inbound turns. Fixes #81229. Thanks @giodl73-repo.
- QA/RTT: let `pnpm rtt` lease Convex-backed Telegram credentials while preserving RTT sample counts, sample timeouts, and result stats on the RTT harness path.
- Discord: bind delayed gateway `identify` retries to the originating socket generation so retries triggered after a reconnect do not identify against a fresh socket. Fixes #82225. Thanks @giodl73-repo.
- ACP/control plane: refresh cached runtime handles when agent config changes so ACP sessions stop using stale runtimes after `agents.defaults` edits. Fixes #82237. Thanks @giodl73-repo.
- Gateway/sessions: scope session data lookups by agent id so multi-agent gateway state cannot cross-leak session records across configured agents. (#81386) Thanks @pgondhi987.
- Gateway/restart: mark active main sessions as restart-aborted before forced restarts so startup recovery can resume interrupted turns instead of leaving them stranded as running. Fixes #82433. (#82772) Thanks @joshavant.
- Gateway/heartbeat: report heartbeat runner failures with background-specific copy instead of foreground `/new` recovery guidance. Fixes #82708. (#82848) Thanks @joshavant.
- Agents/media: require generated music/video completion agents to use the message tool for visible delivery and stop merging generated image attachments into message-tool-only source reply mirrors, avoiding direct fallback posts that can duplicate media the model already sent.
- Agents/media: accept generated media attachments on internal completion events and report delivery-loss failures as errors, so completed background music/video tasks do not disappear after provider success.
- Matrix/approvals: release in-flight reaction bindings when the channel approval handler stops mid-delivery, preventing stale approval targets after restart. Fixes #82485. (#82482) Thanks @Feelw00.
- Matrix/E2EE: stop requesting MSC4222 `state_after` sync responses so homeservers with incomplete state-after data do not leave fresh encrypted rooms without outbound room encryptors. Fixes #82515. Thanks @nickdecooman.
- TUI: update the displayed model in real time when an auto-fallback resolution swaps in a different model mid-turn, so the status line reflects the actual model handling the run. Fixes #82296. Thanks @giodl73-repo.
- Gateway/sessions: estimate context usage from local/OpenAI-compatible transcripts when provider usage telemetry is missing, so status no longer shows empty usage for real local-model sessions. Fixes #73990. (#82317) Thanks @giodl73-repo.
- Update/installers: override npm `min-release-age` quarantine for OpenClaw-managed package installs, so `openclaw update`, plugin updates, and hosted installer scripts can install the requested latest release immediately.
- Agents/sessions: preserve fresh post-compaction token snapshots across stale usage updates, preventing repeated auto-compaction after every message. Fixes #82576. (#82578) Thanks @njuboy11.
- Agents/replies: preserve active inbound reply context at the LLM boundary so Discord referenced-message turns do not answer from stale session history. Fixes #82608. (#82801) Thanks @joshavant.
- Agents/sessions: expose session transcript lock stale and max-hold tuning, and release the embedded run's coarse transcript lock before model I/O while locking persistence and cleanup separately. Fixes #13744. Thanks @amknight.
- Agents/OpenAI Responses: log redacted diagnostics for detail-less `response.failed` events while preserving failed response ids, so operators can correlate provider-side failures. Fixes #82558.
- Agents/OpenRouter: strip non-replayable Anthropic/xAI reasoning provenance tags from follow-up requests, preventing poisoned thinking signatures from breaking second turns. Fixes #82335. (#82380) Thanks @hclsys.
- Providers/xAI: send configurable reasoning effort only for Grok 4.3, preserving xAI's default low reasoning while omitting unsupported controls for Grok 4.20 reasoning models. (#81227) Thanks @jason-allen-oneal.
- Image generation: raise Google, OpenRouter, and xAI hosted provider default timeouts to 180 seconds so slow hosted image requests have more time to complete. (#75337)
- Agents/auth: redact OAuth refresh failure causes against in-memory, attempted, and reloaded credentials before generic token masking while ensuring failed ACP dispatch cleanup closes initialized runtimes.
- Google/Gemini CLI OAuth: add provider-owned refresh support for `google-gemini-cli` so expired Gemini CLI tokens refresh in OpenClaw instead of falling through to the generic unknown-provider path. Fixes #42541. Thanks @jason-allen-oneal.
- Agents/Anthropic transport: replay `reasoning_content` from compatible thinking blocks for Xiaomi/MiMo-style Anthropic Messages routes, preventing follow-up turns from losing required reasoning context. Fixes #81261. Thanks @Sunnyone2three.
- Telegram: cache successful startup bot identity by account and token fingerprint for up to 24 hours, so restarts can skip redundant `getMe` probes during Telegram API slow periods without permanently pinning renamed bots. Refs #82525.
- Telegram: keep streamed text replies in place when delayed TTS audio arrives, sending the audio as a follow-up instead of deleting the preview. Fixes #82570. (#82820) Thanks @joshavant.
- Channels/TTS: deliver TTS supplements across live-preview channels without duplicating text replies, covering WebChat, Telegram, Discord, Slack, Mattermost, and Matrix. (#82935) Thanks @joshavant.
- Gateway/sessions: discard stale metadata when recreating dead main session rows, so replacement sessions do not inherit old labels or transcript paths.
- Codex app-server: mark native context compaction completion events as successful, preventing false "Compaction incomplete" notices after successful Codex-managed compaction. Fixes #82470. (#81593) Thanks @Kyzcreig.
- Codex app-server: keep long-running turns alive while current-turn approvals, user input, dynamic tools, and notifications make progress, and carry that progress into the outer run timeout. (#82601) Thanks @100yenadmin.
- Gateway/channels: hand off traced channel account startup outside the startup diagnostic phase so long-lived channel tasks do not keep liveness warnings pinned to channel startup. Refs #82398.
- Gateway/restart: queue restart and shutdown signals received while the gateway startup loop is still returning its server handle, so startup-time restarts are not dropped during update churn. (#82660) Thanks @samzong.
- Gateway/restart: carry operator restart intent reasons into macOS LaunchAgent restart traces, so cascade diagnostics identify `gateway.restart` instead of a bare SIGTERM.
- GitHub Copilot: route device-login requests through the plugin SSRF guard with a GitHub-only policy.
- Group/channel replies: keep message-tool-preferred final replies private when the agent misses the message tool, and log suppressed payload metadata in the gateway debug log for quieter diagnosis.
- Gateway/WebChat: route image attachments through a configured vision-capable `imageModel` plan before inlining images, and carry that image-model fallback chain through runtime retries. (#82524) Thanks @frankekn.
- macOS app: open the Dashboard in a native WebKit window with standard macOS traffic-light controls, keep the Dock icon visible by default, and reuse the app's connected gateway auth for automatic Control UI login.
- WebChat: show progress while manual `/compact` is running by streaming a session operation event to subscribed Control UI clients. Fixes #82407. Thanks @Conan-Scott.
- Codex app-server: limit canonical OpenAI Codex app-server attribution rewrites to local transcript and trajectory records, leaving runtime/tool routing on the selected OpenAI model metadata so OpenAI API-key backup profiles keep their billing path.
- Codex app-server: hide native tool-search control tools from dynamic tool exposure while preserving the message tool.
- Android/chat: make bare and markdown URLs in chat messages tappable by preserving Compose URL annotations in rendered markdown. Fixes #82187. (#82392) Thanks @neeravmakwana.
- Plugins/doctor: migrate legacy top-level plugin `tools` declarations into `contracts.tools`, so `openclaw doctor --fix` repairs local plugins for the manifest tool contract. (#81112) Thanks @100yenadmin.
- Slack: guide agents to use stable `<@USER_ID>` mention tokens from context instead of plain `@name` text, so user mentions link and notify correctly. Fixes #82090. (#82152) Thanks @neeravmakwana.
- Auth: serialize provider login writes through the auth-profile lock for OpenAI Codex, Anthropic, Cloudflare AI Gateway, GitHub Copilot, and z.ai, preserving upsert semantics so a live Gateway cannot overwrite freshly refreshed OAuth credentials with an expired in-memory snapshot.
- Auth/Codex: remove runtime support for `oauthRef` sidecar-backed OAuth profiles and add a doctor repair that migrates affected Codex profiles back to inline `auth-profiles.json` credentials. (#82777) Thanks @joshavant.
- Slack: keep DM thread replies on the main direct-message session instead of routing them to invisible thread-scoped sessions. Refs #82390. (#82418) Thanks @kagura-agent.
- Auth/macOS: avoid creating the OAuth profile master key in Keychain automatically, falling back to the file-backed secret key so headless agents do not trigger a Keychain prompt.
- Codex app-server: release raw assistant completions when `turn/completed` is missing while keeping commentary/status items as progress, preventing completed Codex runs from hanging until timeout. Fixes #82343. (#82403) Thanks @IWhatsskill.
- Codex app-server: keep a bounded terminal guard after post-tool raw assistant completions so missing `turn/completed` events fail fast instead of leaving embedded runs stuck. Fixes #82775. (#82816) Thanks @joshavant.
- Agents/sessions: remove the transient `*.bak-<pid>-<ts>` backup written by `repairSessionFileIfNeeded` once the atomic replace succeeds, so a stuck session with a persistently malformed JSONL line no longer accumulates one snapshot per repair invocation. Fixes #80960. (#80969) Thanks @100yenadmin. Co-authored by @tynamite.
- CLI/status: show plain empty-state messages instead of empty Channels and Sessions tables when no channels or sessions exist.
- CLI/dashboard: probe Gateway readiness before handing out the dashboard URL, prompting to start or install the managed service when the Gateway is stopped and printing recovery commands instead of opening a dead browser tab.
- CLI/dashboard: treat Gateway `device identity required` probes as proof that the dashboard listener is reachable, so `openclaw dashboard` can still open the Control UI.
- CLI: hide decorative startup and status emoji on terminals that are unlikely to render them correctly, keeping semantic message and identity emoji intact.
- CLI/gateway: recover the Linux user systemd bus environment when `openclaw dashboard` starts the Gateway from stripped desktop shells such as VNC terminals.
- Gateway/WebSocket: log expected startup `1013 gateway starting` retry closes at debug instead of warn while preserving WARN for unexpected pre-connect failures. Fixes #76361. (#82457) Thanks @IWhatsskill.
- Providers/Xiaomi: strip synthetic empty array `items` from MiMo tool schemas while preserving typed array items, avoiding strict OpenAI-compatible schema rejection.
- Telegram: send the transcript-backed full final answer after progress-mode tool drafts when the dispatcher final payload is an ellipsis-truncated snapshot. Fixes #82409. Thanks @PashaGanson.
- Providers/Ollama: omit truthy native `think` payloads for models marked non-reasoning while preserving supported thinking models and explicit `think: false`. (#82445) Thanks @leno23.
- Update/channels: preserve pre-update channel config through package-swap doctor and post-core plugin repair so externalized channel upgrades do not drop configured chat channels. Fixes #82533. Thanks @imbaig.
- Update/doctor: repair configured externalized plugin installs during legacy 2026.4.x upgrades so configured Discord channels remain available after 2026.5.x package updates. Fixes #82813. (#82859) Thanks @joshavant.
- CLI/context engines: bootstrap and finalize non-legacy context engines for CLI turns while preserving transcript snapshots and deferred maintenance ownership. (#81869) Thanks @sahilsatralkar.
- Telegram: persist polling updates through restart replay so queued same-topic messages resume in order instead of losing context after a gateway restart. (#82256) Thanks @VACInc.
- Gateway/Gmail: abort in-flight Gmail watcher startup and hot-reload restarts before shutdown so reloads cannot spawn `gog serve` after the Gateway is closing. Thanks @frankekn.
- Agents/Codex: fall back to the embedded PI runner when OpenAI's implicit Codex harness preference cannot find a registered Codex plugin, preventing OpenAI-compatible gateway requests from failing with an unregistered harness error. Fixes #82437.
- Agents/OpenAI: honor `openai-codex:*` entries placed ahead of API-key backups in `auth.order.openai` for explicit OpenAI PI runs, and accept `models auth login --provider openai-codex --device-code` for headless sign-in. Fixes #82521. (#82605)
- CLI/channels: install missing externalized same-id channel plugins during `channels add --channel <id>`, so recovery for WhatsApp and other externalized stock channels does not require a separate `plugins enable` step. Fixes #82533.
- Windows node install: launch the node host through a hidden Windows launcher so login startup does not leave a persistent `cmd` window open. Fixes #81254.
- MCP plugin tools: forward host MCP `tools/call` `AbortSignal` through `createPluginToolsMcpHandlers().callTool` into plugin `tool.execute`, so host cancellation actually cancels in-flight plugin tool calls instead of letting them run to completion. Fixes #82424. (#82443) Thanks @joshavant.
- Agents/sandbox: honor explicit Docker sandbox env variables with credential-looking names during container creation, and recreate affected sandbox containers when the effective env policy changes. Fixes #82695. (#82763) Thanks @joshavant.
- Plugins: accept deprecated `api.on("deactivate")` registrations as a dated compatibility alias for `gateway_stop`, so external plugin cleanup handlers run on Gateway shutdown while authors get migration guidance.
- Plugins: resolve bundled entry, dist-runtime, package-state, and public artifact paths from packaged roots, so bundled plugin probes and hardlinked public surfaces no longer fall back to source files or fail during restart. Fixes #78462. Fixes #75797. Refs #76865. Thanks @ginishuh and @ymebosma.
- Media: ignore image MIME and filename hints when bytes sniff as generic containers, so zip/octet-stream payloads mislabeled as images do not become local image media or keep image file extensions when staged.
- Update/doctor: avoid materializing `groupAllowFrom` for channel schemas that reject it, so package-swap doctor repairs do not fail on externalized Slack configs.
- Gateway/media: prevent image filenames from overriding generic non-image byte sniffing, so zip/octet-stream payloads mislabeled as images are offloaded or rejected before they become inline image attachments.
- Plugins/web search: downgrade stale optional provider installs to warnings so Gateway and doctor repair paths keep running after startup provider selection. Refs #82313. Thanks @crackmac.
- Telegram/Gateway: route targeted Telegram `/stop@bot` messages onto the control lane without cached bot metadata and match gateway stop requests across raw/canonical session aliases. (#82298) Thanks @VACInc.
- MS Teams/media: sniff inline `data:image/*` attachment bytes before staging them, skipping payloads that are not actually images.
- WebChat/media: require trusted local-media provenance before preserving local audio reply paths for display, so untrusted audio-looking paths go through normal staging and read-policy checks.
- WebChat: trust local Auto-TTS audio on block-streamed replies, including ACP-dispatched tails, so synthesized browser audio renders instead of being silently dropped. Fixes #82628. (#82701) Thanks @leno23.
- Agents/tool media: preserve trusted local-media provenance when merging generated tool attachments into final reply payloads, so trusted audio/media survives outbound display normalization.
- Anthropic/Claude CLI: write model-scoped `claude-cli` runtime policy when reusing local Claude CLI auth, so upgraded Telegram and Dashboard gateway turns keep using the CLI backend instead of falling through to Anthropic API billing. Fixes #82344. Thanks @amknight.
- Update: let package-swap `doctor --fix` persist core config repairs while plugin schemas are still converging, preventing update failures on externalized channel configs.
- Update: carry plugin-validation bypasses into config mutation pre-write reads, so package update doctor repairs can finish while externalized plugin schemas are converging.
- Update/doctor: keep plugin-validation bypasses on the top-level `$include` config write path, so package repair can update included plugin config files without flattening them into the root config.
- Agents/subagents: warn and continue completion announce cleanup when lifecycle cleanup fails, preventing ended subagent runs from becoming silent ghosts. Fixes #82306. Thanks @SebTardif.
- Telegram: let authorized text `/stop` commands use the fast-abort path before queued agent work, so active turns stop immediately instead of processing the abort after the turn finishes; foreign-bot `/stop@otherbot` mentions now stay on the regular topic lane instead of being routed into our control lane. Fixes #82162. Thanks @civiltox.
- Sessions: drop persisted entries with invalid session ids and strip malformed transcript file metadata before hydrating session runtime state.
- Auth/device: normalize malformed persisted device-auth token metadata before returning or preserving token entries.
- Pairing: skip malformed persisted pending pairing requests before approving valid channel pairing codes.
- Commitments: strip malformed optional reminder scope metadata from persisted commitments before matching pending follow-ups.
- Config persistence: normalize malformed auth profile credential fields/state, skip JSON-valid garbage transcript checkpoint rows, and let `openclaw doctor --fix` remove unrepairable cron job rows.
- Cron: skip persisted job rows with malformed schedule or payload shapes in memory, leaving the store for `openclaw doctor --fix` instead of hydrating them into runtime state.
- Cron: keep legacy string schedules and blank system-event jobs available for runtime repair/skip handling instead of dropping them as malformed persisted rows.
- Task persistence: drop malformed array/scalar requester-origin JSON from task and task-flow SQLite sidecars instead of restoring it as delivery metadata.
- Agents/timeouts: clarify model idle-timeout errors and docs so provider `timeoutSeconds` is shown as bounded by the whole agent/run timeout ceiling.
- Agents/OpenAI streams: yield cooperatively while processing bursty Completions and Responses chunks, keeping aborts, channel liveness timers, and startup heartbeats responsive under noisy model output. Refs #82462.
- Media/images: avoid broad model/plugin discovery while preparing image requests, preventing Windows event-loop stalls that could block Telegram polling. Fixes #82338. (#82799) Thanks @joshavant.
- Release tooling: align the published launcher Node floor, `npm start`, package script checks, sharded lint locking, Vitest root project coverage, and plugin-SDK declaration build cache metadata so release/package validation does not silently skip or ship stale surfaces.
- Cron/agents: honor configured subagent model fallbacks for isolated scheduled runs and forward that fallback policy into embedded agent timeout failover. Fixes #74985. Thanks @chrisgwynne.
- Codex app-server/MCP: scope user MCP servers to specific OpenClaw agent ids through an optional `mcp.servers.<name>.codex.agents` list and accept `codex.defaultToolsApprovalMode` (`auto`/`prompt`/`approve`) for native Codex approval defaults; OpenClaw strips the `codex` block before handing `mcp_servers` config to Codex. (#82180) Thanks @sercada.
- Agents/OpenAI Responses: clamp `input_tokens - cached_tokens` at zero and reconstruct `totalTokens` from input + output + cached components so Responses-API streams report consistent usage when providers under-report `input_tokens` relative to `cached_tokens`.
- Agents: mark adapter-caught tool execution failures as error tool results in embedded Pi sessions, so models can retry recoverable edit failures instead of seeing a successful tool result. Fixes #81546. (#81564) Thanks @najef1979-code and @MonkeyLeeT.
- Plugins: reject malformed `package.json` `openclaw.extensions` metadata during install, discovery, and post-update payload smoke instead of silently dropping invalid entries.
- Plugins: reject package metadata records whose `package.json` resolves outside the plugin root instead of trusting persisted or reconstructed registry snapshots.
- Plugins: ignore malformed persisted package channel/install metadata instead of crashing catalog reconstruction or leaking invalid install hints.
- Plugin releases: reject package `files` negations that would omit advertised package-local runtime entries from npm plugin tarballs.
- Media/files: sniff `input_file` bytes before trusting declared MIME headers, rejecting spoofed image or zip payloads before they become agent-visible text.
- Plugins/dependencies: scrub stale managed-root `openclaw` ownership metadata without deleting a linked active host package, preventing plugin installs from downgrading npm-global hosts. Fixes #79462. Thanks @lisandromachado.
- Gateway/update: keep shutdown hook-runner imports on a stable dist entry and ship a legacy chunk alias so package swaps do not strand running gateways on missing shutdown chunks. Fixes #81819. Thanks @najef1979-code.
- Config persistence: ignore malformed array/scalar auth profile, cron job state, and session store entries instead of hydrating them into numeric profile ids, crashed cron rows, or invalid session records.
- Config persistence: strip malformed pending final-delivery session fields on load so replay/recovery paths skip poisoned reply metadata instead of crashing on raw objects.
- Config persistence: strip malformed plugin extension state and promoted session-slot ownership on load so corrupted session rows do not leak poisoned plugin metadata into replay/projection paths.
- Gateway/sessions: ignore malformed compaction checkpoint rows during session projection so corrupted stores do not crash session list/describe responses or show bogus checkpoint counts.
- Gateway/sessions: keep reachable transcript history when imported tree transcripts reference missing or legacy parent rows, preventing session history reads from going empty after a partial import.
- Trajectory export: report incomplete transcript parent chains and stop cyclic branch walks so malformed imports cannot hang `/export-trajectory`.
- Session replay: skip malformed user/assistant-shaped transcript rows during silent session resets instead of copying invalid entries into the fresh transcript.
- Transcript state: skip malformed persisted JSONL entries before compaction/rewrite helpers choose the active leaf.
- Backup verify: report malformed archive manifests with a stable error instead of leaking raw JSON parser details.
- Session export: report skipped malformed transcript JSONL rows instead of silently omitting them from exported HTML archives.
- Providers: reject malformed successful Runway, BytePlus, and Ollama embedding responses with provider-owned errors instead of raw parser/type failures, silent bad vectors, or long bogus polling.
- Providers/images: reject malformed successful OpenAI-compatible, OpenAI, Google, fal, and OpenRouter image responses with provider-owned errors instead of raw shape failures, silent invalid base64 skips, or empty image results.
- Providers/videos: reject malformed successful xAI, OpenRouter, and fal video create, poll, and result responses with provider-owned errors instead of raw parser failures or long bogus polling.
- Providers/videos: let selected-model capability overlays clear inherited `providerOptions`, so fallback skips models that explicitly accept no provider-specific options instead of forwarding unsupported knobs.
- TTS/providers: honor preferred provider aliases when routing model override directives, so alias-selected speech providers receive unqualified `[[tts:*]]` overrides.
- Providers/audio: reject malformed successful OpenAI-compatible, ElevenLabs, and Deepgram speech responses with provider-owned errors instead of raw parser failures, wrong-shaped transcripts, or JSON/text bodies treated as audio.
- Providers/embeddings: reject malformed successful OpenAI-compatible, Google Gemini, and Amazon Bedrock embedding responses instead of silently returning empty or coerced vectors.
- Providers/catalogs: reject malformed successful LM Studio, GitHub Copilot, DeepInfra, Vercel AI Gateway, and Kilocode model-list responses with provider-owned errors instead of raw parser/type failures or silent fallback catalogs.
- Providers/polling: reject array, null, or scalar successful operation status responses with provider-owned malformed JSON errors instead of waiting until timeout.
- ACPX/Codex: reap plugin-local Codex ACP adapter orphans on startup after wrapper crashes while keeping direct adapter commands out of launch-lease injection. Fixes #82364. (#82459) Thanks @joshavant.
- Agents/model fallback: periodically probe the configured primary for auto-pinned fallback sessions, announce fallback/recovery transitions, and clear the pin when it recovers, preventing sessions from staying on a fallback model indefinitely. Fixes #82544. Thanks @crpol.
- Telegram: send presentation-only payloads by rendering fallback text and inline buttons instead of treating them as empty. Fixes #82404. (#82449) Thanks @joshavant.
- Providers/Kimi: preserve Kimi Coding `reasoning_content` replay and backfill assistant tool-call placeholders when thinking is enabled, so `kimi-for-coding` follow-up tool turns no longer fail after prior tool use. Fixes #82161. Thanks @amknight.
- Providers/search tools: reject malformed successful xAI, Gemini, and Kimi web/code search responses with provider-owned errors instead of silent `No response` payloads or ungrounded fallback state.
- Trajectory export: skip and report malformed session/runtime JSONL rows in `manifest.json` instead of letting wrong-shaped session rows crash support bundle export.
- Voice calls: persist rejected inbound-call replay keys so duplicate carrier webhook retries stay ignored after a Gateway restart.
- Config/doctor: copy fallback-enabled channel `allowFrom` entries into explicit `groupAllowFrom` allowlists during `openclaw doctor --fix`, preserving current group access without adding runtime fallback-transition flags.
- Config/doctor: replace source-only official Brave and Slack plugin installs from trusted catalog metadata during `openclaw doctor --fix`, unblocking externalized stock plugin recovery after upgrade. (#82425) Thanks @joshavant.
- Config/memory: warn instead of rejecting configs that select the official external `memory-lancedb` slot before the plugin is installed, with an explicit no-persistent-memory startup warning and install hint. Fixes #82428. (#82438) Thanks @giodl73-repo.
- Agents/bootstrap: ignore stale completed root `BOOTSTRAP.md` context after workspace setup cleanup fails, preventing channel agent turns from treating it as a directory. (#82463) Thanks @joshavant.
- Update/doctor: re-enable the Codex plugin during `openclaw doctor --fix` when configured OpenAI agent models require the Codex runtime, preventing upgraded configs from failing with an unregistered Codex harness. Fixes #82368. (#82502) Thanks @joshavant.
- Configure: show one OpenAI provider entry with ChatGPT/Codex sign-in and API key choices, and keep browsed Codex models in the saved `/model` picker allowlist.
- Agents/model fallback: preserve auto fallback chains across deferred config reloads when session fallback provenance survives but `modelOverrideSource` is missing. Fixes #81982. Thanks @joshavant.
- Hooks: raise bounded gateway lifecycle hook wait budgets to 5 seconds for shutdown and 10 seconds for pre-restart, giving short restart notification handlers time to finish before shutdown continues. (#82273) Thanks @bryanbaer.
@@ -51,6 +419,7 @@ Docs: https://docs.openclaw.ai
- Cron: keep failed isolated-agent runs from marking successful result delivery when only the failure notification was delivered. Fixes #72985. Thanks @Allenbluff.
- Discord: validate message-read results before normalizing channel history and report unexpected payloads with a Discord boundary error instead of `map is not a function`. Fixes #82252. Thanks @jessewunderlich.
- Agents/runtime: apply `agents.defaults.models["provider/*"].agentRuntime` as provider-wide model runtime policy while preserving exact model runtime precedence. Fixes #82243. Thanks @rendrag-git.
- Model picker: show the effective Codex runtime first for official OpenAI routes while keeping Pi available as an alternate and preserving Pi-first custom OpenAI-compatible providers. Fixes #82269. Thanks @rendrag-git.
- Agents/auto-reply: restrict `NO_REPLY` prompt guidance to automatic group/channel replies, remove legacy silent-reply rewrites, and suppress accidental direct-chat silent tokens instead of delivering fallback text. Fixes #82254. Thanks @absol89.
- Telegram: retain a longer partial-stream preview when a final callback only carries an ellipsis-truncated snapshot, preventing the visible answer and transcript mirror from being replaced by the short preview. Fixes #82239. Thanks @crash2kx.
- Telegram/active-memory: run blocking memory recall through the Telegram provider for direct-message turns even when the hook context carries the raw chat id, preventing embedded recall from launching against an invalid numeric channel. Fixes #82177. Thanks @cslash-zz.
@@ -59,7 +428,9 @@ Docs: https://docs.openclaw.ai
- Telegram: drain queued outbound deliveries after polling reconnect confirms fresh `getUpdates` activity, so stale-socket and network recovery do not leave failed replies stranded. Fixes #50040. Refs #82175. Thanks @dmitriiforpost-commits and @shellyrocklobster.
- Gateway/model auth: abort active provider runs when saved auth is removed through the Gateway control plane, refresh live runtime auth snapshots, and surface `stopReason: "auth-revoked"` to clients. Fixes #81987. (#82346) Thanks @joshavant.
- Codex app-server: keep the raw tool-output idle watchdog armed after `custom_tool_call_output` notifications, so post-tool stream silence fails fast instead of waiting for the terminal idle timeout. Fixes #82274. (#82378) Thanks @joshavant.
- Codex app-server: enforce OpenClaw `before_tool_call` policy for Codex-native app-server shell and approval paths, preventing native tool execution from bypassing plugin policy. Fixes #82372. (#82496) Thanks @joshavant.
- Telegram: mark isolated polling ingress unhealthy when a spooled inbound backlog stalls while Bot API polling still succeeds, so gateway/channel health no longer stays green after Telegram DM processing wedges. Fixes #82175. Thanks @shellyrocklobster.
- Telegram: drop expired approval callbacks from isolated polling after approval id expiry so stale inline-button updates do not retry forever across restarts. Fixes #82347. (#82455) Thanks @joshavant.
- Agents: strip Gemini/Gemma `<final>` tags with attributes or self-closing syntax from delivered replies, including strict final-tag streaming enforcement. Fixes #65867. Thanks @grizdum.
- macOS/update: disarm legacy `ai.openclaw.update.*` LaunchAgents when `openclaw update` starts from one, preventing KeepAlive relaunch loops that repeatedly restart the Gateway and replay update continuations. Fixes #82167. Thanks @DougButdorf.
- Agents/replay: strip internal runtime-context metadata and `NO_REPLY` sentinels from provider replay and pending final-delivery recovery so restart and heartbeat resumes do not feed control text back to the model. Fixes #76629. Thanks @fuyizheng3120, @bryan-chx, and @cael-dandelion-cult.
@@ -77,6 +448,8 @@ Docs: https://docs.openclaw.ai
- Providers/OpenRouter: stop adding empty DeepSeek V4 `reasoning_content` placeholders to assistant tool-call replay messages and strip empty replay artifacts before follow-up Chat Completions requests, so `openrouter/deepseek/deepseek-v4-pro` no longer fails after tool use. Fixes #82150. (#82158) Thanks @luyao618 and @Suquir0.
- OpenAI-compatible providers: honor streaming-usage compatibility metadata when deciding whether to send `stream_options.include_usage`, while keeping bundled Volcengine routes opted in to Ark streaming usage. Refs #44845. (#82181) Thanks @xuruiray.
- Gateway/approvals: treat `turnSourceTo` as optional in `canBridgeNoDeviceChatApprovalFromBackend`, matching the existing optional handling of `turnSourceAccountId` and `turnSourceThreadId`. Channels without a recipient concept (webchat, control-ui) leave `turnSourceTo` null on both the approval snapshot and the replay params, so the prior required-string check rejected every backend replay with `APPROVAL_CLIENT_MISMATCH`. Cross-channel replay is still gated by the required `turnSourceChannel` and `sessionKey` checks. Fixes #82132. (#82136) Thanks @ottodeng.
- OC Path: add `openclaw path set --dry-run --diff` so addressed edits can be reviewed as a unified diff before writing.
- Cron: load runtime plugins before isolated cron model and delivery resolution so external channels can be selected for scheduled runs. (#82111) Thanks @medns.
- Cron: mirror successful direct scheduled deliveries into the resolved destination session transcript while preserving isolated-delivery awareness policy. (#80786) Thanks @cavit99.
- Cron: preserve rotated transcript identity after session-bound scheduled runs compact, so `sessionTarget: "current"` keeps the next user message on the same conversation. Fixes #82164. Thanks @weissfl.
@@ -97,6 +470,9 @@ Docs: https://docs.openclaw.ai
- Providers: preserve required `reasoning_content` replay for Kimi K2.6/K2 thinking and MiMo V2.6 OpenAI-compatible tool-call follow-up turns while keeping the stock OpenAI/Qwen strip path intact. Fixes #82139. Thanks @yimao.
- Memory search: stop using chokidar write-stability polling for memory and QMD watchers so large Markdown extraPath trees no longer build up regular file descriptors; changed files now settle through the existing debounced sync queue. Fixes #77327 and #78224. (#81802) Thanks @frankekn, @loyur, and @JanPlessow.
- Message tool: rename the Discord channel-create schema field exposed to models from `type` to `channelType`, avoiding NVIDIA NIM JSON Schema parser failures while still accepting legacy `type` tool calls. (#78920) Thanks @YashSaliya.
- Feishu: send CardKit streaming cards as delivered deltas and retry failed updates, preventing duplicated or dropped streamed text. Fixes #82417. (#82419) Thanks @hclsys.
- WhatsApp: accept `group:`-prefixed group JIDs for outbound targets so `whatsapp:group:<jid>@g.us` resolves to the canonical group JID. Thanks @mcaxtr.
- Gateway/Gmail: stop queued post-ready Gmail sidecars before hot reload and abort stale Tailscale setup, so cancelled watcher restarts cannot rewrite an old public hook target or report abort-killed commands as success. (#82395) Thanks @samzong.
## 2026.5.14
@@ -114,7 +490,7 @@ Docs: https://docs.openclaw.ai
- Gateway/plugins: add a descriptor-backed gateway method registry so plugin-owned RPC methods carry scope metadata, preserve hidden core collision checks, and keep advertised method lists separate from internal core handlers. (#82063)
- Gateway/startup: add owner-level startup trace attribution for auth, plugin loading, lookup counts, and plugin sidecar services. (#81738) Thanks @samzong.
- Plugins/hooks: expose the resolved effective `contextTokenBudget` plus source/reference metadata on `llm_output` and sanitized `model_call_*` hook events/contexts so plugin cost and context-health alerts can use agent-level context caps. Fixes #64327. Thanks @BunsDev.
- Channels/status reactions: wire `StatusReactionController` into WhatsApp message turns (queued thinking tool done/error lifecycle, on par with Telegram and Discord), add `deploy`/`build`/`concierge` emoji categories with tool-token routing, and replace the status reaction defaults with self-explanatory emoji (🧠 thinking, 🛠️ tool, 💻 coding, 🌐 web, stallSoft, ⚠️ stallHard, done, error, 🗜️ compacting) so stall and lifecycle reactions read as status indicators instead of emotional commentary. Fixes #59077. (#80612) Thanks @gado-ships-it.
- Channels/status reactions: wire `StatusReactionController` into WhatsApp message turns (queued → thinking → tool → done/error lifecycle, on par with Telegram and Discord), add `deploy`/`build`/`concierge` emoji categories with tool-token routing, and replace the status reaction defaults with self-explanatory emoji (🧠 thinking, 🛠️ tool, 💻 coding, 🌐 web, ⏳ stallSoft, ⚠️ stallHard, ✅ done, ❌ error, 🗜️ compacting) so stall and lifecycle reactions read as status indicators instead of emotional commentary. Fixes #59077. (#80612) Thanks @gado-ships-it.
- Control UI: add a browser-local Text size setting in Appearance and Quick Settings, scaling chat and dense UI text while keeping inputs above the mobile Safari focus-zoom threshold. Fixes #8547. Thanks @BunsDev.
- Gateway/plugins: add a default-off `admin-http-rpc` plugin for selected control-plane methods, with security docs and no core endpoint config. (#81806) Thanks @liorb-mountapps.
- Docs: add a dedicated ds4 provider page with local DeepSeek V4 Flash config, on-demand startup, context sizing, and live verification steps.
@@ -201,7 +577,7 @@ Docs: https://docs.openclaw.ai
- Codex account/status: hide empty rate-limit buckets and show server-reported usage-limit blocks without calling them available.
- Auto-reply/Claude CLI: bridge CLI-runtime assistant text-delta agent events into the chat reasoning preview through `onReasoningStream`, mirroring the existing assistant-text (#76914) and tool-event (#80046) bridges and adding gating so non-CLI runtimes are unaffected. Thanks @anagnorisis2peripeteia and @pashpashpash.
- Mantis: keep QA evidence in Actions artifacts only and stop publishing evidence files to Git-backed artifact branches.
- CLI/migrate: handle delayed Codex plugin marketplace responses so warnings, next-steps, and conflict states render with ⚠️ glyphs and post-install migration retries the marketplace fetch instead of silently skipping plugin items. (#81625) Thanks @sjf.
- CLI/migrate: handle delayed Codex plugin marketplace responses so warnings, next-steps, and conflict states render with ⚠️ glyphs and post-install migration retries the marketplace fetch instead of silently skipping plugin items. (#81625) Thanks @sjf.
- Channels/Weixin: bump the bundled `@tencent-weixin/openclaw-weixin` external entry to `2.4.3` (from `2.4.1`) so onboarding and `openclaw channels add` install the current Tencent Weixin (personal WeChat) plugin release. (#81730) Thanks @scotthuang.
- CLI: lazy-load model, plugin, and device runtime helpers and keep channel option help on generated startup metadata or generic fallback text so parent/help output renders without importing those runtime paths.
- CLI: route `plugins list --json` through the parsed command fast path and cover it in response budgets so plugin JSON inventory avoids full CLI registration work.
@@ -299,7 +675,7 @@ Docs: https://docs.openclaw.ai
- CLI/migrate: hide per-item source/plugin hints on non-conflicting Codex skill and plugin selection prompts, keeping the hint text reserved for rows that actually need attention. Thanks @sjf.
- Codex harness: treat high-confidence app-server OAuth refresh invalidation as a terminal auth-profile failure, stopping repeated raw token-refresh errors without turning entitlement or usage-limit payloads into re-auth prompts.
- CLI/migrate: humanize Codex conflict-status messaging across the migrate UI so selection prompts and plan/result rows say "Codex skill already installed in workspace" instead of surfacing internal `MIGRATION_REASON_*` codes. Thanks @sjf.
- CLI/migrate: render migrate result rows with distinct glyphs for manual-review (🔍) and archive (📖) items instead of the misleading "skipped" and "migrated" checkmarks, so users can see which entries still need attention versus which were filed away. Thanks @sjf.
- CLI/migrate: render migrate result rows with distinct glyphs for manual-review (🔍) and archive (📖) items instead of the misleading "skipped" and "migrated" checkmarks, so users can see which entries still need attention versus which were filed away. Thanks @sjf.
- CLI/migrate: split Codex migrate output into separate preview and result phases so the Before plan and After result render through clack with independently tunable copy. Thanks @sjf.
- Codex app-server: project bundle and user MCP servers into Codex threads, rotate threads when an MCP server is disabled, scope bundle MCP injection to bundled servers, and resend user MCP config on resume so MCP changes take effect mid-session without restarting the agent. (#81551) Thanks @jalehman.
- Codex migration: invoke the managed Codex binary instead of a stale system `codex` for source-config migration plans, so users running the bundled Codex runtime get plan output that matches the binary the gateway will actually use. (#81582) Thanks @fuller-stack-dev.
@@ -333,12 +709,30 @@ Docs: https://docs.openclaw.ai
- OpenAI plugin: clarify remote Codex OAuth login copy so tunneled users know sign-in may finish automatically before they paste the redirect URL. (#81301) Thanks @rubencu.
- SGLang: preserve replayed reasoning history for OpenAI-compatible chat completions, keeping thinking-capable local models from losing prior reasoning turns. (#81091) Thanks @akrimm702.
- Plugins/install: derive managed peer dependency pins from npm's lockfile planner instead of recursively scanning `node_modules`, while keeping OpenClaw host peers out of managed root ownership and preserving active root-managed runtimes. Thanks @fuller-stack-dev.
- OC Path: restore YAML/YML/.lobster support through the bundled YAML document parser and add `$first` positional addressing alongside `$last`.
- Control UI/WebChat: keep short assistant replies clear of in-bubble copy/open action buttons by applying the existing reserved action spacing in the grouped chat renderer. Fixes #79509. (#81244) Thanks @JARVIS-Glasses.
- Codex harness: make the live test wrapper portable to Windows and defer locked temp cleanup so native Windows and WSL2 live runs complete.
- Telegram: discard legacy long-poll update offsets that cannot be tied to the current bot token, so token rotation no longer leaves bots silently skipping new messages. (#80671) Thanks @sxxtony.
- browser: enforce navigation checks for act interactions [AI]. (#81070) Thanks @pgondhi987.
- Validate node exec event provenance [AI]. (#81071) Thanks @pgondhi987.
- Gateway: keep active reply runs visible to stuck-session diagnostics and clear no-active-work recovery state, preventing stale queued lanes after compaction or tool failures. Fixes #80677. (#81302)
- Codex app-server: rotate incompatible context-engine-managed native threads so Lossless-managed sessions do not resume stale hidden Codex history. (#81223) Thanks @jalehman.
- Codex cron: execute scheduled command-style automation payloads before workspace bootstrap or memory review, preserving existing isolated cron jobs after Codex harness migration. (#81510) Thanks @jalehman.
- Plugin LLM completions: honor Codex agent-runtime policy for canonical OpenAI model refs, so context-engine summarizers can use Codex OAuth instead of requiring direct `OPENAI_API_KEY` auth. (#81511) Thanks @jalehman.
- Gateway/OpenAI HTTP: return OpenAI-compatible 400 errors for invalid sampling params and provider validation failures instead of collapsing them to 500s. (#81275) Thanks @Lellansin.
- Telegram: publish plugin and skill command description localizations to native command menus while filtering unsupported locale codes and preserving Telegram command limits. (#81351) Thanks @jzakirov.
- Limit hook CLI tool authority [AI]. (#81065) Thanks @pgondhi987.
- Require admin scope for node device token management [AI]. (#81067) Thanks @pgondhi987.
- Restrict chat sender allowlist matching [AI]. (#80898) Thanks @pgondhi987.
- Update: suppress the false newer-config warning during restart health probing after an update handoff, while keeping future-version mutation guards intact. (#78652)
- Claude CLI: clear a reused stored session id after aborts or non-expired failover errors so the next turn does not resume a poisoned CLI session. Fixes #78785.
- Sessions: redact persisted tool result detail metadata before writing transcripts so diagnostic secrets do not survive tool output redaction. (#80444) Thanks @nimbleenigma.
- Codex runtime: allow the official installed `@openclaw/codex` package to use its private task-runtime and MCP projection SDK helpers, fixing `MODULE_NOT_FOUND` during migrated OpenAI/Codex beta runs.
- Codex migration: make Enter activate the highlighted checkbox row before continuing, so `Skip for now` and bulk-selection rows work even when planned items start preselected.
- Link understanding: fetch page content through the SSRF guard before running configured CLI summarizers, preventing curl/wget-style link fetchers from reaching private redirect or DNS-rebound targets.
- fix: harden safe-bin argument validation [AI]. (#80999) Thanks @pgondhi987.
- Codex/status: align `/codex status` rate-limit wording with `/status` by showing remaining quota and compact reset durations instead of used quota and raw ISO timestamps. Thanks @MatthewSchleder.
- Mattermost: log a structured `mattermost no-visible-reply` diagnostic when a substantive (non-reasoning) final reply payload reaches `deliverMattermostReplyPayload` but the underlying `deliverTextOrMediaReply` returns `"empty"` previously the run completed with a misleading `delivered reply to <channel>` log even though no Mattermost API send happened, masking silent completions in channel/thread contexts. No behavior change; the diagnostic surfaces the failure so operators can detect it instead of seeing the agent appear to go silent. Fixes #80501. Thanks @robbyproc87.
- Mattermost: log a structured `mattermost no-visible-reply` diagnostic when a substantive (non-reasoning) final reply payload reaches `deliverMattermostReplyPayload` but the underlying `deliverTextOrMediaReply` returns `"empty"` — previously the run completed with a misleading `delivered reply to <channel>` log even though no Mattermost API send happened, masking silent completions in channel/thread contexts. No behavior change; the diagnostic surfaces the failure so operators can detect it instead of seeing the agent appear to go silent. Fixes #80501. Thanks @robbyproc87.
- Telegram: limit concurrent startup `getMe` probes across multi-account bots so large Telegram configs do not fan out all account probes at once during gateway startup. Refs #80695. (#80986) Thanks @stainlu.
- fix(config): reject auto-managed meta.lastTouched\* paths in config set/unset (#80856). Thanks @ai-hpc
- Test state: seed isolated auth-profile secret keys for generated homes, preventing helper-backed proof runs from falling back to host Keychain secrets. (#81393) Thanks @altaywtf.
@@ -350,7 +744,7 @@ Docs: https://docs.openclaw.ai
- Gateway: throttle assistant/thinking agent event fanout during streaming bursts without dropping buffered deltas. (#80335) Thanks @samzong.
- Models: restore authenticated CLI runtime providers in the `/models` picker while keeping legacy runtime aliases hidden from setup/default model choices. Closes #81212. (#81239) Thanks @anagnorisis2peripeteia.
- Changelog gates: reject bot/app handles as `Thanks` attribution and require explicit human credit for bot/app-authored changelog entries. (#81357) Thanks @hxy91819.
- Agents/heartbeat: fix seven layered issues that broke multi-agent heartbeat cadence (1) fan out the scheduler broadcast wake across agents in parallel via `Promise.all` instead of awaiting each `runOnce` sequentially, so one agent doing real work no longer starves every later agent in iteration order; (2) scope `skipWhenBusy` to lanes attributable to the firing agent via session-key parsing of `session:agent:<id>:` / `nested:agent:<id>:` lane names, instead of consulting the global `subagent` lane, so a single stuck subagent on one agent no longer silently disables every other agent's heartbeat; (3) always append workspace `HEARTBEAT.md` directives (everything outside an optional `tasks:` block) to the dispatch prompt, so prose-runbook `HEARTBEAT.md` files reach the model directly instead of being silently dropped unless periodic tasks are declared; (4) race the initial stream-establishment promise inside `streamWithIdleTimeout` against the same watchdog timer that previously only guarded inter-token gaps, so SDK requests stuck at TCP/TLS handshake or before the first response byte no longer hang indefinitely (the stalled-session diagnostic's `recovery=none` case); (5) emit an `openclaw doctor` warning when `heartbeat.session` pins a session key that has no entry in the agent's session store, so silently-dropped heartbeat deliveries surface at config-validation time; (6) also route the commitment-only task dispatch path (tasks configured, none due) through `appendHeartbeatFileDirectives` so prose directives outside the `tasks:` block reach the model on this path as well; (7) wrap the synchronous `baseFn(...)` invocation inside `streamWithIdleTimeout` in a try/catch that clears the connect watchdog timer before rethrowing, so a provider stream function that throws during setup no longer leaves a live timer that can fire `onIdleTimeout` later with a stale error and keep the process open past the real failure. Thanks @zeroaltitude.
- Agents/heartbeat: fix seven layered issues that broke multi-agent heartbeat cadence — (1) fan out the scheduler broadcast wake across agents in parallel via `Promise.all` instead of awaiting each `runOnce` sequentially, so one agent doing real work no longer starves every later agent in iteration order; (2) scope `skipWhenBusy` to lanes attributable to the firing agent via session-key parsing of `session:agent:<id>:…` / `nested:agent:<id>:…` lane names, instead of consulting the global `subagent` lane, so a single stuck subagent on one agent no longer silently disables every other agent's heartbeat; (3) always append workspace `HEARTBEAT.md` directives (everything outside an optional `tasks:` block) to the dispatch prompt, so prose-runbook `HEARTBEAT.md` files reach the model directly instead of being silently dropped unless periodic tasks are declared; (4) race the initial stream-establishment promise inside `streamWithIdleTimeout` against the same watchdog timer that previously only guarded inter-token gaps, so SDK requests stuck at TCP/TLS handshake or before the first response byte no longer hang indefinitely (the stalled-session diagnostic's `recovery=none` case); (5) emit an `openclaw doctor` warning when `heartbeat.session` pins a session key that has no entry in the agent's session store, so silently-dropped heartbeat deliveries surface at config-validation time; (6) also route the commitment-only task dispatch path (tasks configured, none due) through `appendHeartbeatFileDirectives` so prose directives outside the `tasks:` block reach the model on this path as well; (7) wrap the synchronous `baseFn(...)` invocation inside `streamWithIdleTimeout` in a try/catch that clears the connect watchdog timer before rethrowing, so a provider stream function that throws during setup no longer leaves a live timer that can fire `onIdleTimeout` later with a stale error and keep the process open past the real failure. Thanks @zeroaltitude.
- Matrix: stop running `npm install`/`pnpm install` at runtime from a parent-derived plugin path; missing Matrix runtime dependencies now fail with repair guidance instead of mutating the wrong `node_modules` tree. Fixes #80758. (#80876) Thanks @kinjitakabe.
- Agents/memory-flush: surface non-abort memory-flush failures (provider timeout, transport error, generic agent failure) as visible reply payloads so the outer reply loop short-circuits and isolated cron runs propagate the error into `meta.error` instead of completing silently with `status: "ok"` and an empty payload. Previously only the specific "Memory flush writes are restricted to ..." message was surfaced. Fixes #80755. Thanks @nailujac.
- Channels/loop-guard: enforce shared per-pair bot loop protection in the core channel-turn kernel, with Discord, Slack, Matrix, and Google Chat supplying bot-pair facts where they can reliably identify accepted bot-authored messages. The generic guard keys on `(scope, conversation, participant pair)`, suppresses every additional bot-to-bot event in either direction once a pair crosses the configured budget, and lifts suppression after `cooldownSeconds`. Defaults are `maxEventsPerWindow: 20`, `windowSeconds: 60`, and `cooldownSeconds: 60` whenever a channel lets bot-authored messages reach dispatch; they can be set globally via `channels.defaults.botLoopProtection` and overridden per channel/account or supported per-conversation config. Fixes #58789. Thanks @pandadev66.
@@ -414,7 +808,6 @@ Docs: https://docs.openclaw.ai
- Plugin SDK: remove the owner-specific `provider-auth-login` public subpath after moving Chutes, GitHub Copilot, and OpenAI Codex auth flows back to provider-owned modules.
- Plugin SDK: remove provider-specific model, stream, and xAI compatibility helpers from public exports after moving bundled callers to provider-owned modules.
- Plugin SDK: expose runtime-supplied active model metadata to native plugin tool factories for diagnostics and plugin-owned policy decisions. Fixes #77857. Thanks @jamiezigelbaum.
- Plugin SDK/runtime: add `api.runtime.llm.completeStructured(...)` for host-owned structured plugin inference with optional image inputs, JSON/schema validation, auth-profile selection, and the same model/agent override trust gates as `api.runtime.llm.complete`.
- QA/Mantis: add Telegram live PR evidence automation with Convex-leased credentials, Crabbox transcript capture, motion GIF previews, and inline PR comments.
- QA/Mantis: add a Telegram desktop scenario builder that leases Crabbox, installs native Telegram Desktop, configures an OpenClaw Telegram gateway with leased bot credentials, and records VNC screenshot/video artifacts.
- Discord/voice: add realtime voice diagnostics for speaker turns, playback resets, barge-in detection, and audio cutoff analysis.
@@ -441,7 +834,7 @@ Docs: https://docs.openclaw.ai
- Codex app-server: mirror native Codex subagent spawn lifecycle events into Task Registry so app-server child agents appear in task/status surfaces without relying on transcript text. (#79512) Thanks @mbelinky.
- Skills: add `skills.load.allowSymlinkTargets` so intentional symlinked skill folders can resolve into trusted sibling repos without disabling root containment.
- Agents/tools: add core Tool Search so agents can search and call large OpenClaw, MCP, and client tool catalogs through one compact PI bridge.
- Doctor: warn when a per-agent model config omits the `fallbacks` key and `agents.defaults.model.fallbacks` is non-empty. Covers both string-form (`"model": "..."`) and partial-object form (`"model": { "primary": "..." }`) both silently clobber the defaults chain at runtime. Use `"fallbacks": []` to explicitly opt out of fallbacks, or add `"fallbacks": [...]` to inherit or override. Fixes #79369.
- Doctor: warn when a per-agent model config omits the `fallbacks` key and `agents.defaults.model.fallbacks` is non-empty. Covers both string-form (`"model": "..."`) and partial-object form (`"model": { "primary": "..." }`) — both silently clobber the defaults chain at runtime. Use `"fallbacks": []` to explicitly opt out of fallbacks, or add `"fallbacks": [...]` to inherit or override. Fixes #79369.
- Chat commands: add `/think default` and `/fast default` to clear session overrides and inherit configured/provider defaults. (#79385) Thanks @VACInc.
- Dependencies: refresh workspace dependency pins and lockfile, including `@openai/codex` `0.130.0`, `acpx` `0.7.0`, AWS SDK `3.1044.0`, OpenTelemetry `0.217.0`, `typebox` `1.1.38`, `vite` `8.0.11`, `oxfmt` `0.48.0`, and `oxlint` `1.63.0`, and update the Codex harness model snapshot for the new bundled app-server catalog.
- Plugins/install: add guarded plugin install overrides so onboarding and repair tests can route specific plugins to registry specs or local `npm pack` artifacts via environment variables.
@@ -616,6 +1009,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- Agents: honor `OPENCLAW_WORKSPACE_DIR` when resolving the default agent workspace, preserving explicit config precedence while keeping env-backed deployments out of the system prompt fallback path. Fixes #66786.
- Doctor/Codex: stop warning that the message tool is unavailable for source-reply paths where OpenClaw grants `message` at runtime, keeping update and doctor output aligned with the OpenAI happy path. Thanks @pashpashpash.
- Channels/Weixin: bump the external Weixin catalog entry to `@tencent-weixin/openclaw-weixin@2.4.3` with the matching package integrity. (#81730) Thanks @scotthuang.
- Agents/subagents: apply `agents.defaults.subagents.model` before target agent primary models during `sessions_spawn`, so model-scoped runtimes such as `claude-cli` stay attached to default child runs. Fixes #81395. (#81783) Thanks @joshavant.
@@ -654,6 +1048,7 @@ Docs: https://docs.openclaw.ai
- Codex app-server: keep per-agent `CODEX_HOME` isolation without rewriting `HOME` by default, so Codex-run subprocesses can still find normal user-home config, tokens, and CLI state unless the launch explicitly overrides `HOME`. Thanks @pashpashpash.
- iMessage: stop sending visible `<media:image>` placeholder text for media-only native image sends while preserving the internal echo key that prevents self-echo duplicate replies. (#81209) Thanks @homer-byte.
- Agents/sessions: create configured agent main sessions before first `sessions_send` or gateway send, so agent-to-agent messages no longer fail when the target agent has not started yet.
- Google models: honor configured `reasoning: false` when resolving thinking policy, preventing non-thinking Google/Gemma models from advertising `thinking=medium`. Fixes #81424.
- gateway: pass Talk session scope to resolver [AI]. (#81379) Thanks @pgondhi987.
- Gateway protocol: require v4 clients and stream explicit chat `deltaText`/`replace` frames so SDK clients can consume assistant updates without local diffing. (#80725) Thanks @samzong.
- GitHub Copilot: exchange OAuth tokens for Copilot API tokens on image understanding requests and route Gemini image payloads through Chat Completions, fixing Copilot Gemini image descriptions. (#80393, #80442) Thanks @afunnyhy.
@@ -698,6 +1093,7 @@ Docs: https://docs.openclaw.ai
- Require admin scope for node device token management [AI]. (#81067) Thanks @pgondhi987.
- Restrict chat sender allowlist matching [AI]. (#80898) Thanks @pgondhi987.
- Update: suppress the false newer-config warning during restart health probing after an update handoff, while keeping future-version mutation guards intact. (#78652)
- Bundled MCP: inline local `$ref` parameter schemas before exposing tools, so Notion-style `oneOf` inputs validate through the bridge. Fixes #78737.
- Sessions: redact persisted tool result detail metadata before writing transcripts so diagnostic secrets do not survive tool output redaction. (#80444) Thanks @nimbleenigma.
- Codex runtime: allow the official installed `@openclaw/codex` package to use its private task-runtime and MCP projection SDK helpers, fixing `MODULE_NOT_FOUND` during migrated OpenAI/Codex beta runs.
- Codex migration: make Enter activate the highlighted checkbox row before continuing, so `Skip for now` and bulk-selection rows work even when planned items start preselected.
@@ -730,7 +1126,7 @@ Docs: https://docs.openclaw.ai
- WhatsApp: externalize the channel as a ClawHub/npm plugin outside the core npm runtime bundle, and bump Baileys to `7.0.0-rc11` so libsignal resolves from the registry instead of a GitHub tarball.
- WhatsApp: keep optional audio decoding dependencies local to the external plugin so the core npm install no longer pulls WhatsApp-only media helpers.
- Build: skip copied metadata for bundled plugins that are excluded from build entries, preventing update/status rebuilds from advertising missing QQ Bot runtime files. (#80925)
- Control UI/sessions: nest subagent sessions under their parent session in the session picker dropdown using a visual `└─ ` prefix, making the parent-child relationship clear. Fixes #77628. (#78623) Thanks @chinar-amrutkar.
- Control UI/sessions: nest subagent sessions under their parent session in the session picker dropdown using a visual `└─ ` prefix, making the parent-child relationship clear. Fixes #77628. (#78623) Thanks @chinar-amrutkar.
- Auto-reply: surface a visible error when the configured model backend fails and fallback produces no visible reply, while preserving intentional silent turns and side-effect-only deliveries. (#80917) Thanks @dutifulbob.
- Agents/exec: skip redundant heartbeat wake-ups for subagent session exec completions, preventing spurious LLM invocations on parent sessions. Fixes #66748. (#66749) Thanks @ggzeng.
- Provider streams: keep OpenAI-compatible SSE and JSON fallback streams draining across split chunks and fail Azure Responses streams with a bounded first-event diagnostic instead of stalling. Refs #80926. (#80927) Thanks @galiniliev and @CaptainTimon.
@@ -944,7 +1340,7 @@ Docs: https://docs.openclaw.ai
- Google/Gemini: normalize retired Gemini 3 Pro Preview ids inside provider auth config patches so setup-emitted provider catalogs test `google/gemini-3.1-pro-preview`.
- GitHub Copilot: mint short-lived Copilot API tokens with the same `vscode-chat` integration identity used by runtime requests, and refresh legacy cached tokens missing that identity so image-capable Copilot models no longer inherit the `copilot-language-server` scope. Fixes #79946, #80074. Thanks @TurboTheTurtle.
- Plugins/doctor: drop stale managed npm install records when `openclaw doctor --fix` removes npm packages that shadow bundled plugins, so the rebuilt registry no longer resurrects the removed package metadata.
- Doctor: warn when a per-agent model config omits the `fallbacks` key and `agents.defaults.model.fallbacks` is non-empty. Covers both string-form (`"model": "..."`) and partial-object form (`"model": { "primary": "..." }`) both silently clobber the defaults chain at runtime. Use `"fallbacks": []` to explicitly opt out of fallbacks, or add `"fallbacks": [...]` to inherit or override. Fixes #79369. Thanks @Kaspre.
- Doctor: warn when a per-agent model config omits the `fallbacks` key and `agents.defaults.model.fallbacks` is non-empty. Covers both string-form (`"model": "..."`) and partial-object form (`"model": { "primary": "..." }`) — both silently clobber the defaults chain at runtime. Use `"fallbacks": []` to explicitly opt out of fallbacks, or add `"fallbacks": [...]` to inherit or override. Fixes #79369. Thanks @Kaspre.
- Discord/voice: reuse or suppress late realtime consult tool calls without stealing newer speaker context or speaking forced fallback answers twice.
- Discord/voice: skip likely incomplete realtime forced-consult transcript fragments and non-actionable closings so stale partial speech does not queue delayed answers over the next turn.
- Discord/voice: keep realtime forced consults from clearing active exact-speech playback, so back-to-back voice answers queue instead of cutting each other off.
@@ -1056,6 +1452,7 @@ Docs: https://docs.openclaw.ai
- Agents/compaction: cap summarization output reserve tokens to the selected model's `maxTokens` so 1M-context Anthropic compactions do not request more output than the API permits. Fixes #54383.
- Control UI/login: replace raw connection failures with structured, actionable login guidance for auth, pairing, insecure HTTP, origin, protocol, and transport failures. Thanks @BunsDev.
- Agents/tools: fail `exec host=node` before `system.run` when the selected node is known to be disconnected, with an actionable reconnect message instead of a raw node invoke failure. Thanks @BunsDev.
- Agents/tool-result guard: ignore internal tool-result `details` when estimating model-visible context, so large diagnostic metadata no longer triggers unnecessary truncation or compaction even though the provider boundary already strips `details` before model conversion. (#75525) Thanks @zqchris.
- Agents/models: accept legacy `anthropic-cli/*` model refs as Claude CLI runtime refs instead of failing model resolution with `Unknown model`. Thanks @BunsDev.
- Agents/tools: keep restrictive-profile tool-section warnings scoped to the configured sections whose tools are still missing from `alsoAllow`, so already re-allowed filesystem tools do not make exec-only fixes look broader than they are. Thanks @BunsDev.
- Agents/tools: avoid warning messaging-only agents about inherited global `tools.exec` or `tools.fs` sections when the agent profile did not configure those tool sections itself. Thanks @BunsDev.
@@ -1087,7 +1484,7 @@ Docs: https://docs.openclaw.ai
- Discord/groups: instruct group-chat agents to stay silent when a message is addressed to someone else, replying only when invited or correcting key facts. (#78615)
- Discord/groups: tell Discord-channel agents to wrap bare URLs as `<https://example.com>` so link previews do not expand into uninvited embeds. (#78614)
- Agents/fallback: fail fast on session write-lock timeouts instead of trying fallback models for local file contention. Fixes #66646. Thanks @sallyom.
- Browser/SSRF: stop closing user-owned Chrome tabs when a read-only operation (snapshot/screenshot/interactions) is rejected by the SSRF guard only OpenClaw-initiated navigations now close on policy denial. Thanks @scotthuang.
- Browser/SSRF: stop closing user-owned Chrome tabs when a read-only operation (snapshot/screenshot/interactions) is rejected by the SSRF guard — only OpenClaw-initiated navigations now close on policy denial. Thanks @scotthuang.
- iMessage: stage native inbound attachments into OpenClaw-managed media and convert HEIC/HEIF images to JPEG before dispatch, so image tools can read photos sent over native iMessage without requiring BlueBubbles.
- Agents/Gateway: throttle and cap live exec command-output events so noisy tool runs cannot flood Gateway WebSocket clients or starve RPC handling. (#78645) Thanks @joshavant.
- Memory Wiki: skip empty and whitespace-only source pages when refreshing generated Related blocks, preventing blank pages from being rewritten into Related-only stubs. Fixes #78121. Thanks @amknight.
@@ -1154,9 +1551,10 @@ Docs: https://docs.openclaw.ai
- Plugins/CLI: load the install-records ledger when listing channel-catalog entries, so npm-installed third-party channel plugins resolve through `openclaw channels login`/`channels add` instead of failing with `Unsupported channel`. (#77269) Thanks @pumpkinxing1.
- Memory wiki/Security: enforce session visibility on shared-memory `wiki_search` and `wiki_get` so sandboxed subagents cannot read transcript content from sibling or parent sessions. Fixes GHSA-72fw-cqh5-f324. Thanks @zsxsoft.
- Exec approvals: enforce allowlist `argPattern` argument restrictions on Linux and macOS as well as Windows, so an entry like `{ pattern: "python3", argPattern: "^safe\.py$" }` no longer silently relaxes to a path-only match on non-Windows hosts. (#75143) Thanks @eleqtrizit.
- Security/exec allowlist: collapse `.` and `..` segments in wildcard exec allowlist match targets and canonicalize absolute executable path candidates before regex matching, so a target like `/usr/bin/../../bin/sh` no longer string-matches a `/usr/bin/**` allowlist entry while resolving outside the declared root. (#75723) Thanks @eleqtrizit and @zsxsoft.
- Agents/compaction: disable Pi auto-compaction whenever OpenClaw effectively owns safeguard compaction, including provider-backed safeguard mode, so Pi and OpenClaw no longer fight over long-session compaction. Fixes #73003. (#73839) Thanks @bradhallett.
- Telegram/streaming: finalize text replies by stopping the edited stream message instead of sending a second answer bubble, so Telegram turns cannot duplicate the streamed final response. (#77947) Thanks @obviyus.
- web_search/Brave: fix provider selection when Brave is installed as an external plugin and `tools.web.search.provider: "brave"` is explicitly configured a redundant provider re-resolution at startup could race and return an empty list, causing a spurious `WEB_SEARCH_PROVIDER_INVALID_AUTODETECT` warning and treating the explicitly configured provider as absent. Fixes #77676. Thanks @openperf.
- web_search/Brave: fix provider selection when Brave is installed as an external plugin and `tools.web.search.provider: "brave"` is explicitly configured — a redundant provider re-resolution at startup could race and return an empty list, causing a spurious `WEB_SEARCH_PROVIDER_INVALID_AUTODETECT` warning and treating the explicitly configured provider as absent. Fixes #77676. Thanks @openperf.
- Doctor/plugins: discover doctor contracts from load-path channel plugins during `openclaw doctor --fix`, so plugin-owned legacy config repair runs before validation. (#77477) Thanks @jalehman.
- Dependencies: bump transitive `basic-ftp` to 5.3.1 so the runtime lockfile no longer includes the vulnerable 5.3.0 build flagged by the production dependency audit. (#78637) Thanks @sallyom.
- Hooks/cron: log returned `/hooks/agent` isolated-run errors and failed cron jobs with cron diagnostic summaries, so rejected `payload.model` values are visible instead of looking like accepted-but-missing runs. Fixes #78597. (#78655) Thanks @kevinslin.
@@ -1331,9 +1729,9 @@ Docs: https://docs.openclaw.ai
- Plugins/performance: let unscoped model catalog and manifest-contract readers reuse the current workspace-compatible plugin metadata snapshot, avoiding repeated cold plugin metadata scans on hot control-plane paths while preserving env/config/workspace compatibility checks. (#77519, #77532)
- Config/plugin auto-enable: prefer the claiming plugin manifest id over a built-in channel alias when auto-allowlisting a configured channel, so WeCom/Yuanbao-style aliases resolve to the installed plugin id. Thanks @Beandon13.
- Secrets/apply: preserve auth-profile `keyRef` and `tokenRef` fields when scrubbing provider-target secrets, so the canonical SecretRef metadata survives `secrets apply` without keeping plaintext values. Thanks @Beandon13.
- Plugins/active-memory: skip session-store channel entries that contain `:` when resolving the recall subagent's channel, so QQ c2c agent IDs (e.g. `c2c:10D4F7C2`) and other scoped conversation IDs do not reach bundled-plugin `dirName` validation and crash the recall run. The same guard already applied to explicit `channelId` params (#76704); this extends it to store-derived channels. (#77396) Thanks @hclsys.
- Plugins/active-memory: skip session-store channel entries that contain `:` when resolving the recall subagent's channel, so QQ c2c agent IDs (e.g. `c2c:10D4F7C2…`) and other scoped conversation IDs do not reach bundled-plugin `dirName` validation and crash the recall run. The same guard already applied to explicit `channelId` params (#76704); this extends it to store-derived channels. (#77396) Thanks @hclsys.
- Secrets/external channel contracts: also look in `<rootDir>/dist/` when resolving the `secret-contract-api` sidecar, so npm-published externalized channel plugins (e.g. `@openclaw/discord` since 2026.5.2) whose compiled artifacts live under `dist/` actually contribute their channel SecretRef contracts to the runtime snapshot. Without this, env-backed `channels.discord.token` SecretRefs silently failed to resolve at gateway start on 2026.5.3, leaving the channel `not configured` even though #76449 had landed the generic external-contract loader. Thanks @mogglemoss.
- Models/auth: add `openclaw models auth list [--provider <id>] [--json]` so users can inspect saved per-agent auth profiles without dumping secrets or hitting the old too many arguments path. Thanks @vincentkoc.
- Models/auth: add `openclaw models auth list [--provider <id>] [--json]` so users can inspect saved per-agent auth profiles without dumping secrets or hitting the old “too many arguments” path. Thanks @vincentkoc.
- Control UI/header: show the active agent name in dashboard breadcrumbs without adding the current session key, keeping non-chat views oriented without crowding the topbar.
- Control UI/cron: make the New Job sidebar collapsible so the jobs list can reclaim space while keeping the form one click away. Thanks @BunsDev.
- Gateway/startup: keep model-catalog test helpers, run-session lookup code, QR pairing helpers, and TypeBox memory-tool schema construction out of hot startup import paths, reducing default gateway benchmark plugin-load and memory pressure.
@@ -1658,6 +2056,7 @@ Docs: https://docs.openclaw.ai
- Telegram: preserve URL inline keyboard buttons in shared presentation rendering. Fixes #76255. Thanks @clawSean.
- Update: repair doctor-migratable legacy config before persisting `openclaw update --channel ...`, so old Slack/Telegram streaming keys do not block switching to beta after a package update. Thanks @vincentkoc.
- Plugins/bundles: preserve explicit `activation` metadata from Codex, Cursor, and Claude bundle manifests in registry records, so bundle startup opt-outs are not treated as legacy implicit startup sidecars. (#75133) Thanks @100menotu001.
- Web fetch: late-bind `web_fetch` config and provider fallback metadata from the active runtime snapshot, matching `web_search` so long-lived tools do not use stale fetch provider settings. Thanks @vincentkoc.
- Plugins/discovery: demote the source-only TypeScript runtime check on already-installed `origin: "global"` plugin packages from a config-blocking error to a warning and let the runtime fall through to the TypeScript source via jiti, so a single broken installed package no longer blocks `plugins install` for unrelated plugins; install-time rejection of newly-installed source-only packages is unchanged. Thanks @romneyda.
- Providers/OpenAI Codex: stop the OAuth progress spinner before showing the manual redirect paste prompt, so callback timeouts do not spam `Browser callback did not finish` across terminals.
@@ -1835,7 +2234,7 @@ Docs: https://docs.openclaw.ai
- CLI/plugins: reject missing plugin ids before config writes in `plugins enable` and `plugins disable` so a typo no longer persists a stale config entry. (#73554) Thanks @ai-hpc.
- Agents/sessions: preserve delivered trailing assistant replies during session-file repair so Telegram/WebChat history is not rewritten to drop already-delivered responses. Fixes #76329. Thanks @obviyus.
- Gateway/chat history: preserve oversized transcript turns as explicit omitted-message placeholders while avoiding large JSONL parse stalls. Thanks @Marvinthebored and @vincentkoc.
- CLI/doctor: load the configured memory-slot plugin when resolving memory diagnostics so bundled `memory-core` no longer triggers a false no active memory plugin warning on standalone `doctor` / `status` runs. Fixes #76367. Thanks @neeravmakwana.
- CLI/doctor: load the configured memory-slot plugin when resolving memory diagnostics so bundled `memory-core` no longer triggers a false “no active memory plugin” warning on standalone `doctor` / `status` runs. Fixes #76367. Thanks @neeravmakwana.
- Gateway: preserve stack diagnostics when `chat.send` or agent attachment parsing/staging fails, improving image-send failure triage. Refs #63432. (#75135) Thanks @keen0206.
- Agents/idle-timeout: add a cost-runaway breaker to the outer embedded-run retry loop that halts further attempts after 5 consecutive idle timeouts without completed model progress, so a wedged provider can no longer fan paid model calls out across the same run; completed text or tool-call progress resets the breaker, but partial tool-argument token dribbles do not. Fixes #76293. Thanks @ThePuma312.
- Heartbeats/Codex: align structured heartbeat prompts with actual `heartbeat_respond` tool availability, stop sending legacy `HEARTBEAT_OK` when the tool exists, and keep tool-disabled commitment check-ins on the legacy ack path. Thanks @pashpashpash and @vincentkoc.
@@ -1914,7 +2313,7 @@ Docs: https://docs.openclaw.ai
- Slack: allow draft preview streaming in top-level DMs when `replyToMode` is `off` while keeping Slack native streaming and assistant thread status gated on reply threads. Fixes #56480. (#56544) Thanks @HangGlidersRule.
- Control UI/chat: remove the delete-confirm popover outside-click listener on every dismiss path, so Cancel, Delete, outside clicks, and same-button toggles no longer leave stale document listeners behind. Refs #75590 and #69982. Thanks @Ricardo-M-L.
- Memory-core: treat exhausted file watcher limits as non-fatal for builtin memory auto-sync while preserving fatal handling for unrelated disk-full errors. (#73357) Thanks @solodmd.
- Providers/Ollama: restore catalog context-window forwarding as `num_ctx` for native `/api/chat` requests; fixes tool selection and context truncation regressions on models with catalog entries (qwen3, llama3, gemma3, ) when no explicit `params.num_ctx` was configured. Fixes #76117. (#76181) Thanks @openperf.
- Providers/Ollama: restore catalog context-window forwarding as `num_ctx` for native `/api/chat` requests; fixes tool selection and context truncation regressions on models with catalog entries (qwen3, llama3, gemma3, …) when no explicit `params.num_ctx` was configured. Fixes #76117. (#76181) Thanks @openperf.
- Plugins/install: pin npm plugin installs to the verified resolved version and reject package-lock version or integrity drift, so mutable tags cannot race integrity checks into accepting a different artifact. Thanks @Lucenx9.
- Exec approvals: preserve trusted elevated defaults across approved command follow-up runs so same-turn elevated `on`/`ask` commands request a fresh approval instead of reporting elevated as unavailable. Fixes #75832. Thanks @jameyedwards and @bitloi.
@@ -2260,7 +2659,7 @@ Docs: https://docs.openclaw.ai
- Slack: publish a safe default App Home tab view on `app_home_opened` and include the Home tab event in setup manifests. Fixes #11655; refs #52020. Thanks @TinyTb.
- Slack: keep track of bot-participated threads across restarts, so ongoing threaded conversations can continue auto-replying after the Gateway is restarted. Thanks @amknight.
- Control UI/Usage: add UTC quarter-hour token buckets for the Usage Mosaic and reuse them for hour filtering, keeping the legacy session-span fallback for older summaries. (#74337) Thanks @konanok.
- BlueBubbles: add opt-in `channels.bluebubbles.replyContextApiFallback` that fetches the original message from the BlueBubbles HTTP API when the in-memory reply-context cache misses (multi-instance deployments sharing one BB account, post-restart, after long-lived TTL/LRU eviction). Off by default; channel-level setting propagates to accounts that omit the flag through `mergeAccountConfig`; routed through the typed `BlueBubblesClient` so every fetch is SSRF-guarded by the same three-mode policy as every other BB client request; reply-id shape is validated and part-index prefixes (`p:0/<guid>`) are stripped before the request; concurrent webhooks for the same `replyToId` coalesce into one fetch and successful responses populate the reply cache for subsequent hits. Also promotes BlueBubbles attachment download failures from verbose to runtime error so silently-dropped inbound images are visible at default log level, and extends `sanitizeForLog` to redact `?password=`/`?token=` query params and `Authorization:` headers before they reach the log sink (CWE-532). (#71820) Thanks @coletebou and @zqchris.
- BlueBubbles: add opt-in `channels.bluebubbles.replyContextApiFallback` that fetches the original message from the BlueBubbles HTTP API when the in-memory reply-context cache misses (multi-instance deployments sharing one BB account, post-restart, after long-lived TTL/LRU eviction). Off by default; channel-level setting propagates to accounts that omit the flag through `mergeAccountConfig`; routed through the typed `BlueBubblesClient` so every fetch is SSRF-guarded by the same three-mode policy as every other BB client request; reply-id shape is validated and part-index prefixes (`p:0/<guid>`) are stripped before the request; concurrent webhooks for the same `replyToId` coalesce into one fetch and successful responses populate the reply cache for subsequent hits. Also promotes BlueBubbles attachment download failures from verbose to runtime error so silently-dropped inbound images are visible at default log level, and extends `sanitizeForLog` to redact `?password=…`/`?token=…` query params and `Authorization:` headers before they reach the log sink (CWE-532). (#71820) Thanks @coletebou and @zqchris.
- CLI/proxy: add `openclaw proxy validate` so operators can verify effective proxy configuration, proxy reachability, and expected allow/deny destination behavior before deploying proxy-routed OpenClaw commands. (#73438) Thanks @jesse-merhi.
- Agents/Codex: default Codex app-server dynamic tools to native-first, keeping OpenClaw integration tools while leaving file, patch, exec, and process ownership to the Codex harness. (#75308) Thanks @pashpashpash.
- Agents/Codex: default Codex-harness direct source replies to the OpenClaw `message` tool when visible reply delivery is not explicitly configured, keeping channel-visible output as a deliberate tool call. (#75765) Thanks @pashpashpash.
@@ -2566,7 +2965,7 @@ Docs: https://docs.openclaw.ai
- Web fetch: add a documented `tools.web.fetch.ssrfPolicy.allowIpv6UniqueLocalRange` opt-in and thread it through cache keys and DNS/IP checks so trusted fake-IP proxy stacks using `fc00::/7` can work without broad private-network access. Fixes #74351. Thanks @jeffrey701.
- OpenAI Codex: restore `/verbose full` persistence and app-server tool-output forwarding, and retry Gateway E2E temp-home cleanup so debug runs do not regress on stale validation or cleanup flakes. Thanks @vincentkoc.
- Anthropic/Meridian: preserve text and thinking content seeded on `content_block_start` in anthropic-messages streams, so `[thinking, text]` replies no longer persist as empty turns or trigger empty-response fallbacks. Fixes #74410. Thanks @vyctorbrzezowski.
- Channels/Matrix: complete the cross-signing handshake on `openclaw matrix verify confirm-sas` so the operator's other Matrix device clears its `Verifying` loop instead of staying stuck after the agent confirms. (#74542) Thanks @nklock.
- Channels/Matrix: complete the cross-signing handshake on `openclaw matrix verify confirm-sas` so the operator's other Matrix device clears its `Verifying…` loop instead of staying stuck after the agent confirms. (#74542) Thanks @nklock.
- CLI/status: honor channel-specific model context-window overrides when reporting effective context, so channel-scoped sessions reflect the active window in `openclaw status`. Thanks @HemantSudarshan.
- Sandbox/Docker: tolerate Docker daemon unavailability when sandbox mode is off, so doctor and preflight checks no longer fail on installs that do not run the Docker daemon. Fixes #73671. Thanks @kaseonedge.
- Control UI/mobile: persist mobile chat settings through Lit-managed state and route mobile navigation through the same view-state path so chat panel toggles survive transitions on small viewports. Thanks @BunsDev.
@@ -3079,7 +3478,7 @@ Docs: https://docs.openclaw.ai
- Plugins: share package entrypoint resolution between install and discovery, reject mismatched `runtimeExtensions`, and cache bundled runtime-dependency manifest reads during scans. Thanks @vincentkoc.
- WhatsApp/Web: keep quiet but healthy linked-device sessions connected by basing the watchdog on WhatsApp Web transport activity, while retaining a longer app-silence cap so frame activity cannot mask a stuck session forever. Fixes #70678; carries forward the focused #71466 approach and keeps #63939 as related configurable-timeout follow-up. Thanks @vincentkoc and @oromeis.
- Discord/gateway: count failed health-monitor restart attempts toward cooldown and hourly caps, and evict stale account lifecycle state during channel reloads so repeated Discord gateway recovery cannot loop on old status. Fixes #38596. (#40413) Thanks @jellyAI-dev and @vashquez.
- TTS/BlueBubbles: pre-transcode synthesized MP3 audio to opus-in-CAF (mono, 24 kHz validated against macOS 15.x Messages.app's native voice-memo CAF descriptor) on macOS hosts before handing the file to BlueBubbles, so iMessage renders the result as a native voice-memo bubble with proper duration and waveform UI instead of a plain file attachment. Adds an opt-in `tts.voice.preferAudioFileFormat` channel capability and a magic-byte sniff for the CAF container so the host-local-media validator (which uses `file-type` and didn't recognize CAF natively) can verify the pre-transcoded buffer. Channels that don't opt in are unaffected. (#72586) Fixes #72506. Thanks @omarshahine.
- TTS/BlueBubbles: pre-transcode synthesized MP3 audio to opus-in-CAF (mono, 24 kHz — validated against macOS 15.x Messages.app's native voice-memo CAF descriptor) on macOS hosts before handing the file to BlueBubbles, so iMessage renders the result as a native voice-memo bubble with proper duration and waveform UI instead of a plain file attachment. Adds an opt-in `tts.voice.preferAudioFileFormat` channel capability and a magic-byte sniff for the CAF container so the host-local-media validator (which uses `file-type` and didn't recognize CAF natively) can verify the pre-transcoded buffer. Channels that don't opt in are unaffected. (#72586) Fixes #72506. Thanks @omarshahine.
- Feishu: retry WebSocket startup failures with monitor-owned backoff while preserving SDK-local heartbeat defaults, so persistent-connection startup failures no longer leave the monitor hung. Fixes #68766; related #42354 and #55532. Thanks @alex-xuweilong, @120106835, @sirfengyu, and @tianhaocui.
- Cron: normalize isolated job tool allowlists before granting the narrow self-removal cron tool path, keeping scheduled jobs aligned with shared tool policy normalization. (#73028) Thanks @jalehman.
@@ -3180,7 +3579,7 @@ Docs: https://docs.openclaw.ai
- Channels/setup: treat bundled channel plugins as already bundled during `channels add` and onboarding, enabling them without writing redundant `plugins.load.paths` entries or path install records. Fixes #72740. Thanks @iCodePoet.
- WhatsApp: honor gateway `HTTPS_PROXY` / `HTTP_PROXY` env vars for QR-login WebSocket connections, while respecting `NO_PROXY`, so proxied networks no longer fall back to direct `mmg.whatsapp.net` connections that time out with 408. Fixes #72547; supersedes #72692. Thanks @mebusw and @SymbolStar.
- Bonjour: default mDNS advertisements to the system hostname when it is DNS-safe, avoiding `openclaw.local` probing conflicts and Gateway restart loops on hosts such as `Lobster` or `ubuntu`. Fixes #72355 and #72689; supersedes #72694. Thanks @mscheuerlein-bot, @gcusms, @moyuwuhen601, @pavan987, @zml-0912, @hhq365, and @SymbolStar.
- Agents/OpenAI-compatible: retry replay-safe empty `stop` turns once for `openai-completions` endpoints, so transient empty local backend responses no longer surface as Agent couldn't generate a response when a continuation succeeds, and restore `openclaw agent --model` for one-shot CLI runs. Fixes #72751. Thanks @moooV252.
- Agents/OpenAI-compatible: retry replay-safe empty `stop` turns once for `openai-completions` endpoints, so transient empty local backend responses no longer surface as “Agent couldn't generate a response” when a continuation succeeds, and restore `openclaw agent --model` for one-shot CLI runs. Fixes #72751. Thanks @moooV252.
- Git hooks: skip ignored staged paths when formatting and restaging pre-commit files, so merge commits no longer abort when `.gitignore` newly ignores staged merged content. Fixes #72744. Thanks @100yenadmin.
- Memory-core/dreaming: add a supported `dreaming.model` knob for Dream Diary narrative subagents, wired through phase config and the existing plugin subagent model-override trust gate. Refs #65963. Thanks @esqandil and @mjamiv.
- Agents/Anthropic: remove trailing assistant prefill payloads when extended thinking is enabled, so Opus 4.7/Sonnet 4.6 requests do not fail Anthropic's user-final-turn validation. Fixes #72739. Thanks @superandylin.
@@ -4075,7 +4474,7 @@ Docs: https://docs.openclaw.ai
- WhatsApp/onboarding: keep first-run setup entry loading off the Baileys runtime dependency path, so packaged QuickStart installs can show WhatsApp setup before runtime deps are staged. Fixes #70932.
- Block streaming: suppress final assembled text after partial block-delivery aborts when the already-sent text chunks exactly cover the final reply, preventing duplicate replies without dropping unrelated short messages. Fixes #70921.
- Codex harness/Windows: resolve npm-installed `codex.cmd` shims through PATHEXT before starting the native app-server, so `codex/*` models work without a manual `.exe` shim. Fixes #70913.
- Slack/groups: classify MPIM group DMs as group chat context and suppress verbose tool/plan progress on Slack non-DM surfaces, so internal "Working" traces no longer leak into rooms. Fixes #70912.
- Slack/groups: classify MPIM group DMs as group chat context and suppress verbose tool/plan progress on Slack non-DM surfaces, so internal "Working…" traces no longer leak into rooms. Fixes #70912.
- Agents/replay: stop OpenAI/Codex transcript replay from synthesizing missing tool results while still preserving synthetic repair on Anthropic, Gemini, and Bedrock transport-owned sessions. (#61556) Thanks @VictorJeon and @vincentkoc.
- Telegram/media replies: parse remote markdown image syntax into outbound media payloads on the final reply path, so Telegram group chats stop falling back to plain-text image URLs when the model or a tool emits `![...](...)` instead of a `MEDIA:` token. (#66191) Thanks @apezam and @vincentkoc.
- Agents/WebChat: surface non-retryable provider failures such as billing, auth, and rate-limit errors from the embedded runner instead of logging `surface_error` and leaving webchat with no rendered error. Fixes #70124. (#70848) Thanks @truffle-dev.
@@ -4208,7 +4607,7 @@ 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.
- 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.
@@ -4474,7 +4873,7 @@ Docs: https://docs.openclaw.ai
- Cron/isolated-agent: preserve explicit `delivery.mode: "none"` message targets for isolated runs without inheriting implicit `last` routing, so agent-initiated Telegram sends keep their authored destination while bare `mode:none` jobs stay targetless. (#69153) Thanks @davehappyminion and @nikilster.
- Cron/isolated-agent: keep `delivery.mode: "none"` account-only or thread-only configs from inheriting a stale implicit recipient, so isolated runs only resolve message routing when the job authored an explicit `to` target. (#69163) Thanks @davehappyminion and @nikilster.
- Gateway/TUI: retry session history while the local gateway is still finishing startup, so `openclaw tui` reconnects no longer fail on transient `chat.history unavailable during gateway startup` errors. (#69164) Thanks @shakkernerd.
- BlueBubbles/reactions: fall back to `love` when an agent reacts with an emoji outside the iMessage tapback set (`love`/`like`/`dislike`/`laugh`/`emphasize`/`question`), so wider-vocabulary model reactions like `👀` still produce a visible tapback instead of failing the whole reaction request. Configured ack reactions still validate strictly via the new `normalizeBlueBubblesReactionInputStrict` path. (#64693) Thanks @zqchris.
- BlueBubbles/reactions: fall back to `love` when an agent reacts with an emoji outside the iMessage tapback set (`love`/`like`/`dislike`/`laugh`/`emphasize`/`question`), so wider-vocabulary model reactions like `👀` still produce a visible tapback instead of failing the whole reaction request. Configured ack reactions still validate strictly via the new `normalizeBlueBubblesReactionInputStrict` path. (#64693) Thanks @zqchris.
- BlueBubbles: prefer iMessage over SMS when both chats exist for the same handle, honor explicit `sms:` targets, and never silently downgrade iMessage-available recipients. (#61781) Thanks @rmartin.
- Telegram/setup: require numeric `allowFrom` user IDs during setup instead of offering unsupported `@username` DM resolution, and point operators to `from.id`/`getUpdates` for discovery. (#69191) Thanks @obviyus.
- GitHub Copilot/onboarding: default GitHub Copilot setup to `claude-opus-4.6` and keep the bundled default model list aligned, so new Copilot setups no longer start on the older `gpt-4o` default. (#69207) Thanks @obviyus.
@@ -4501,7 +4900,7 @@ Docs: https://docs.openclaw.ai
- Browser/CDP: allow the selected remote CDP profile host for CDP health and control checks without widening browser navigation SSRF policy, so WSL-to-Windows Chrome endpoints no longer appear offline under strict defaults. Fixes #68108. (#68207) Thanks @Mlightsnow.
- Codex: stop cumulative app-server token totals from being treated as fresh context usage, so session status no longer reports inflated context percentages after long Codex threads. (#64669) Thanks @cyrusaf.
- Browser/CDP: add phase-specific CDP readiness diagnostics and normalize loopback WebSocket host aliases, so Windows browser startup failures surface whether HTTP discovery, WebSocket discovery, SSRF validation, or the `Browser.getVersion` health check failed.
- Browser/CDP: discover Chromes real DevTools websocket from bare `ws://host:port` attach-only roots before declaring the profile down, while still falling back to direct websocket providers that do not expose `/json/version`. Fixes #68027. (#68715) Thanks @visionik.
- Browser/CDP: discover Chrome’s real DevTools websocket from bare `ws://host:port` attach-only roots before declaring the profile down, while still falling back to direct websocket providers that do not expose `/json/version`. Fixes #68027. (#68715) Thanks @visionik.
## 2026.4.18
@@ -4527,7 +4926,7 @@ Docs: https://docs.openclaw.ai
- Plugins/discovery: reuse bundled and global plugin discovery results across workspace cache misses so Windows multi-workspace startup stops redoing the shared synchronous scan. (#67940) Thanks @obviyus.
- Bundled plugins/install: keep staged bundled plugin runtime imports resolving through the packaged Plugin SDK while omitting checkout-only aliases from the dist inventory, so published installs do not fail on repo-local paths.
- Plugins/webhooks: enforce synchronous plugin registration with full rollback of failed plugin side effects, and cache SecretRef-backed webhook auth per route so plugin startup and inbound webhook auth stay deterministic. (#67941) Thanks @obviyus.
- Telegram/polling transport: give the Telegram undici dispatcher pool bounded keep-alive defaults and an explicit lifecycle. Previously every recoverable network error and stall watchdog trip silently replaced the transport, abandoning the old dispatcher pool and its sockets; long-running gateway processes accumulated hundreds of ESTABLISHED connections to `api.telegram.org`, saturating per-IP upstream proxy quotas and causing the actively-used outbound proxy node to time out while every other node still tested healthy. Transports now expose `close()`, `TelegramPollingTransportState` destroys the stale transport on dirty-rebuild, and `TelegramPollingSession` disposes the transport when polling exits backed by a strict per-origin pool cap on every constructed `Agent`, `ProxyAgent`, and `EnvHttpProxyAgent` as defence in depth (#69476).
- Telegram/polling transport: give the Telegram undici dispatcher pool bounded keep-alive defaults and an explicit lifecycle. Previously every recoverable network error and stall watchdog trip silently replaced the transport, abandoning the old dispatcher pool and its sockets; long-running gateway processes accumulated hundreds of ESTABLISHED connections to `api.telegram.org`, saturating per-IP upstream proxy quotas and causing the actively-used outbound proxy node to time out while every other node still tested healthy. Transports now expose `close()`, `TelegramPollingTransportState` destroys the stale transport on dirty-rebuild, and `TelegramPollingSession` disposes the transport when polling exits — backed by a strict per-origin pool cap on every constructed `Agent`, `ProxyAgent`, and `EnvHttpProxyAgent` as defence in depth (#69476).
- Telegram/polling: publish successful `getUpdates` calls as account health liveness, avoid false stall restarts after recoverable `getUpdates` errors, and force Telegram API dispatchers to HTTP/1.1 so stalled polling recovers instead of sitting connected-but-dead (#69476).
- Telegram/ACP bindings: drop persisted DM bindings that still point at missing or failed ACP sessions on restart, while preserving plugin-owned bindings and uncertain store reads. (#67822) Thanks @chinar-amrutkar.
- Telegram/streaming: keep a transient preview on the same Telegram message when auto-compaction retries an in-flight answer, so streamed replies no longer appear duplicated after compaction. (#66939) Thanks @rubencu.
@@ -4535,7 +4934,7 @@ Docs: https://docs.openclaw.ai
- Memory-core: preserve stored vector dimensions during read-only recovery so memory indexes do not lose vector metadata while repairing read-only state.
- Reply/block streaming: preserve post-stream incomplete-turn error payloads after block streaming already emitted content, so users get the warning instead of silence. (#67991) Thanks @obviyus.
- Telegram/streaming: clear the compaction replay guard after visible non-final boundaries so a post-tool assistant reply rotates to a fresh preview instead of editing the pre-compaction message. (#67993) Thanks @obviyus.
- Matrix: fix `sessions_spawn --thread` subagent session spawning thread binding creation, cleanup on session end, and completion-message delivery target resolution now work end-to-end. (#67643) Thanks @eejohnso-ops and @gumadeiras.
- Matrix: fix `sessions_spawn --thread` subagent session spawning — thread binding creation, cleanup on session end, and completion-message delivery target resolution now work end-to-end. (#67643) Thanks @eejohnso-ops and @gumadeiras.
- Slack/streaming: resolve native streaming recipient teams from the inbound user when available, with a monitor-team fallback, so DM and shared-workspace streams target the right recipient more reliably.
- macOS/webchat: enable Undo and Redo in the composer text input by turning on the native `NSTextView` undo manager. (#34962) Thanks @tylerbittner.
- macOS/remote SSH: require an already-trusted host key on the macOS remote command, gateway probe, port tunnel, and pairing probe paths by switching `StrictHostKeyChecking=accept-new` to `StrictHostKeyChecking=yes` and centralizing the shared SSH option fragments in `CommandResolver`, so first-time macOS remote connections no longer silently accept an unknown host key and must be trusted ahead of time via `~/.ssh/known_hosts`. (#68199) Thanks @drobison00.
@@ -4623,10 +5022,10 @@ Docs: https://docs.openclaw.ai
- Agents/failover: treat HTML provider error pages as upstream transport failures for CDN-style 5xx responses without misclassifying embedded body text as API rate limits, while still preserving auth remediation for HTML 401/403 pages and proxy remediation for HTML 407 pages. (#67642) Thanks @stainlu.
- Gateway/skills: bump the cached skills-snapshot version whenever a config write touches `skills.*` (for example `skills.allowBundled`, `skills.entries.<id>.enabled`, or `skills.profile`). Existing agent sessions persist a `skillsSnapshot` in `sessions.json` that reuses the skill list frozen at session creation; without this invalidation, removing a bundled skill from the allowlist left the old snapshot live and the model kept calling the disabled tool, producing `Tool <name> not found` loops that ran until the embedded-run timeout. (#67401) Thanks @xantorres.
- Agents/tool-loop: enable the unknown-tool stream guard by default. Previously `resolveUnknownToolGuardThreshold` returned `undefined` unless `tools.loopDetection.enabled` was explicitly set to `true`, which left the protection off in the default configuration. A hallucinated or removed tool (for example `himalaya` after it was dropped from `skills.allowBundled`) would then loop "Tool X not found" attempts until the full embedded-run timeout. The guard has no false-positive surface because it only triggers on tools that are objectively not registered in the run, so it now stays on regardless of `tools.loopDetection.enabled` and still accepts `tools.loopDetection.unknownToolThreshold` as a per-run override (default 10). (#67401) Thanks @xantorres.
- TUI/streaming: add a client-side streaming watchdog to `tui-event-handlers` so the `streaming · Xm Ys` activity indicator resets to `idle` after 30s of delta silence on the active run. Guards against lost or late `state: "final"` chat events (WS reconnects, gateway restarts, etc.) leaving the TUI stuck on `streaming` indefinitely; a new system log line surfaces the reset so users know to send a new message to resync. The window is configurable via the new `streamingWatchdogMs` context option (set to `0` to disable), and the handler now exposes a `dispose()` that clears the pending timer on shutdown. (#67401) Thanks @xantorres.
- Extensions/lmstudio: add exponential backoff to the inference-preload wrapper so an LM Studio model-load failure (for example the built-in memory guardrail rejecting a load because the swap is saturated) no longer produces a WARN line every ~2s for every chat request. The wrapper now records consecutive preload failures per `(baseUrl, modelKey, contextLength)` tuple with a 5s 10s 20s → … → 5min cooldown and skips the preload step entirely while a cooldown is active, letting chat requests proceed directly to the stream (the model is often already loaded via the LM Studio UI). The combined `preload failed` log line now reports consecutive-failure count and remaining cooldown so operators can act on the real issue instead of drowning in repeated warnings. (#67401) Thanks @xantorres.
- TUI/streaming: add a client-side streaming watchdog to `tui-event-handlers` so the `streaming · Xm Ys` activity indicator resets to `idle` after 30s of delta silence on the active run. Guards against lost or late `state: "final"` chat events (WS reconnects, gateway restarts, etc.) leaving the TUI stuck on `streaming` indefinitely; a new system log line surfaces the reset so users know to send a new message to resync. The window is configurable via the new `streamingWatchdogMs` context option (set to `0` to disable), and the handler now exposes a `dispose()` that clears the pending timer on shutdown. (#67401) Thanks @xantorres.
- Extensions/lmstudio: add exponential backoff to the inference-preload wrapper so an LM Studio model-load failure (for example the built-in memory guardrail rejecting a load because the swap is saturated) no longer produces a WARN line every ~2s for every chat request. The wrapper now records consecutive preload failures per `(baseUrl, modelKey, contextLength)` tuple with a 5s → 10s → 20s → … → 5min cooldown and skips the preload step entirely while a cooldown is active, letting chat requests proceed directly to the stream (the model is often already loaded via the LM Studio UI). The combined `preload failed` log line now reports consecutive-failure count and remaining cooldown so operators can act on the real issue instead of drowning in repeated warnings. (#67401) Thanks @xantorres.
- Agents/replay: re-run tool/result pairing after strict replay tool-call ID sanitization on outbound requests so Anthropic-compatible providers like MiniMax no longer receive malformed orphan tool-result IDs such as `...toolresult1` during compaction and retry flows. (#67620) Thanks @stainlu.
- Gateway/startup: fix spurious SIGUSR1 restart loop on Linux/systemd when plugin auto-enable is the only startup config write; the config hash guard was not captured for that write path, causing chokidar to treat each boot write as an external change and trigger a reload restart cycle that corrupts manifest.db after repeated cycles. Fixes #67436. (#67557) thanks @openperf
- Gateway/startup: fix spurious SIGUSR1 restart loop on Linux/systemd when plugin auto-enable is the only startup config write; the config hash guard was not captured for that write path, causing chokidar to treat each boot write as an external change and trigger a reload → restart cycle that corrupts manifest.db after repeated cycles. Fixes #67436. (#67557) thanks @openperf
- Codex/harness: auto-enable the Codex plugin when `codex` is selected as an embedded agent harness runtime, including forced default, per-agent, and `OPENCLAW_AGENT_RUNTIME` paths. (#67474) Thanks @duqaXxX.
- OpenAI Codex/CLI: keep resumed `codex exec resume` runs on the safe non-interactive path without reintroducing the removed dangerous bypass flag by passing the supported `--skip-git-repo-check` resume arg plus Codex's native `sandbox_mode="workspace-write"` config override. (#67666) Thanks @plgonzalezrx8.
- Codex/app-server: parse Desktop-originated app-server user agents such as `Codex Desktop/0.118.0`, keeping the version gate working when the Codex CLI inherits a multi-word originator. (#64666) Thanks @cyrusaf.
@@ -4656,9 +5055,9 @@ Docs: https://docs.openclaw.ai
- Agents/failover: classify OpenAI-compatible `finish_reason: network_error` stream failures as timeout so model fallback retries continue instead of stopping with an unknown failover reason. (#61784) thanks @lawrence3699.
- Onboarding/channels: normalize channel setup metadata before discovery and validation so malformed or mixed-shape channel plugin metadata no longer breaks setup and onboarding channel lists. (#66706) Thanks @darkamenosa.
- Slack/native commands: fix option menus for slash commands such as `/verbose` when Slack renders native buttons by giving each button a unique action ID while still routing them through the shared `openclaw_cmdarg*` listener. Thanks @Wangmerlyn.
- Feishu/webhook: harden the webhook transport and card-action replay guards to fail closed on missing `encryptKey` and blank callback tokens refuse to start the webhook transport without an `encryptKey`, reject unsigned requests when no key is present instead of accepting them, and drop blank card-action tokens before the dedupe claim and dispatcher. Defense-in-depth over the already-closed monitor-account layer. (#66707) Thanks @eleqtrizit.
- Feishu/webhook: harden the webhook transport and card-action replay guards to fail closed on missing `encryptKey` and blank callback tokens — refuse to start the webhook transport without an `encryptKey`, reject unsigned requests when no key is present instead of accepting them, and drop blank card-action tokens before the dedupe claim and dispatcher. Defense-in-depth over the already-closed monitor-account layer. (#66707) Thanks @eleqtrizit.
- Agents/workspace files: route `agents.files.get`, `agents.files.set`, and workspace listing through the shared `fs-safe` helpers (`openFileWithinRoot`/`readFileWithinRoot`/`writeFileWithinRoot`), reject symlink aliases for allowlisted agent files, and have `fs-safe` resolve opened-file real paths from the file descriptor before falling back to path-based `realpath` so a symlink swap between `open` and `realpath` can no longer redirect the validated path off the intended inode. (#66636) Thanks @eleqtrizit.
- Gateway/MCP loopback: switch the `/mcp` bearer comparison from plain `!==` to constant-time `safeEqualSecret` (matching the convention every other auth surface in the codebase uses), and reject non-loopback browser-origin requests via `checkBrowserOrigin` before the auth gate runs. Loopback origins (`127.0.0.1:*`, `localhost:*`, same-origin) still go through, including the `localhost``127.0.0.1` host mismatch that browsers flag as `Sec-Fetch-Site: cross-site`. (#66665) Thanks @eleqtrizit.
- Gateway/MCP loopback: switch the `/mcp` bearer comparison from plain `!==` to constant-time `safeEqualSecret` (matching the convention every other auth surface in the codebase uses), and reject non-loopback browser-origin requests via `checkBrowserOrigin` before the auth gate runs. Loopback origins (`127.0.0.1:*`, `localhost:*`, same-origin) still go through, including the `localhost`↔`127.0.0.1` host mismatch that browsers flag as `Sec-Fetch-Site: cross-site`. (#66665) Thanks @eleqtrizit.
- Auto-reply/billing: classify pure billing cooldown fallback summaries from structured fallback reasons so users see billing guidance instead of the generic failure reply. (#66363) Thanks @Rohan5commit.
- Agents/fallback: preserve the original prompt body on model fallback retries with session history so the retrying model keeps the active task instead of only seeing a generic continue message. (#66029) Thanks @WuKongAI-CMU.
- Reply/secrets: resolve active reply channel/account SecretRefs before reply-run message-action discovery so channel token SecretRefs (for example Discord) do not degrade into discovery-time unresolved-secret failures. (#66796) Thanks @joshavant.
@@ -4724,7 +5123,7 @@ Docs: https://docs.openclaw.ai
- Browser: keep loopback CDP readiness checks reachable under strict SSRF defaults so OpenClaw can reconnect to locally started managed Chrome. (#66354) Thanks @hxy91819.
- Agents/context engine: compact engine-owned sessions from the first tool-loop delta and preserve ingest fallback when `afterTurn` is absent, so long-running tool loops can stay bounded without dropping engine state. (#63555) Thanks @Bikkies.
- OpenAI Codex/auth: keep malformed Codex CLI auth-file diagnostics on the debug logger instead of stdout so interactive command output stays clean while auth read failures remain traceable. (#66451) Thanks @SimbaKingjoe.
- Discord/native commands: return the real status card for native `/status` interactions instead of falling through to the synthetic ` Done.` ack when the generic dispatcher produces no visible reply. (#54629) Thanks @tkozzer and @vincentkoc.
- Discord/native commands: return the real status card for native `/status` interactions instead of falling through to the synthetic `✅ Done.` ack when the generic dispatcher produces no visible reply. (#54629) Thanks @tkozzer and @vincentkoc.
- Hooks/Ollama: let LLM-backed session-memory slug generation honor an explicit `agents.defaults.timeoutSeconds` override instead of always aborting after 15 seconds, so slow local Ollama runs stop silently dropping back to generic filenames. (#66237) Thanks @dmak and @vincentkoc.
- Media/transcription: remap `.aac` filenames to `.m4a` for OpenAI-compatible audio uploads so AAC voice notes stop failing MIME-sensitive transcription endpoints. (#66446) Thanks @ben-z.
- WhatsApp/Baileys media upload: keep encrypted upload POSTs streaming while still guarding generic-agent dispatcher wiring, so large outbound media sends avoid full-buffer RSS spikes and OOM regressions. (#65966) Thanks @frankekn.
@@ -5176,7 +5575,7 @@ Docs: https://docs.openclaw.ai
- Providers/Ollama: honor the selected provider's `baseUrl` during streaming so multi-Ollama setups stop routing every stream to the first configured Ollama endpoint. (#61678).
- Providers/Ollama: stop warning that Ollama could not be reached when discovery only sees empty default local stubs, while still keeping real explicit Ollama overrides loud when the endpoint is unreachable.
- Providers/xAI: recognize `api.grok.x.ai` as an xAI-native endpoint again and keep legacy `x_search` auth resolution working so older xAI web-search configs continue to load. (#61377) Thanks @jjjojoj.
- Providers/Mistral: send `reasoning_effort` for `mistral/mistral-small-latest` (Mistral Small 4) with thinking-level mapping, and mark the catalog entry as reasoning-capable so adjustable reasoning matches Mistrals Chat Completions API. (#62162) Thanks @neeravmakwana.
- Providers/Mistral: send `reasoning_effort` for `mistral/mistral-small-latest` (Mistral Small 4) with thinking-level mapping, and mark the catalog entry as reasoning-capable so adjustable reasoning matches Mistral’s Chat Completions API. (#62162) Thanks @neeravmakwana.
- OpenAI TTS/Groq: send `wav` to Groq-compatible speech endpoints, honor explicit `responseFormat` overrides on OpenAI-compatible paths, and only mark voice-note output as voice-compatible when the actual format is `opus`. (#62233) Thanks @neeravmakwana.
- Tools/web_fetch and web_search: fix `TypeError: fetch failed` caused by undici 8.0 enabling HTTP/2 by default; pinned SSRF-guard dispatchers now explicitly set `allowH2: false` to restore HTTP/1.1 behavior and keep the custom DNS-pinning lookup compatible. (#61738, #61777) Thanks @zozo123.
- Tools/web search/Exa: show Exa Search in onboarding and configure provider pickers again by marking the bundled Exa provider as setup-visible. Thanks @vincentkoc.
@@ -5374,7 +5773,7 @@ Docs: https://docs.openclaw.ai
- Live model switching: only treat explicit user-driven model changes as pending live switches, so fallback rotation, heartbeat overrides, and compaction no longer trip `LiveSessionModelSwitchError` before making an API call. (#60266) Thanks @kiranvk-2011.
- Exec approvals: reuse durable exact-command `allow-always` approvals in allowlist mode so identical reruns stop prompting, and tighten Windows interpreter/path approval handling so wrapper and malformed-path cases fail closed more consistently. (#59880, #59780, #58040, #59182) Thanks @luoyanglang, @SnowSky1, and @pgondhi987.
- Node exec approvals: keep node-host `system.run` approvals bound to the prepared execution plan across async forwarding, so mutable script operands still get approval-time binding and drift revalidation instead of dropping back to unbound execution.
- Agents/exec approvals: let `exec-approvals.json` agent security override stricter gateway tool defaults so approved subagents can use `security: full` without falling back to allowlist enforcement again. (#60310) Thanks @lml2468.
- Agents/exec approvals: let `exec-approvals.json` agent security override stricter gateway tool defaults so approved subagents can use `security: “full”` without falling back to allowlist enforcement again. (#60310) Thanks @lml2468.
- Agents/exec: restore `host=node` routing for node-pinned and `host=auto` sessions, while still blocking sandboxed `auto` sessions from jumping to gateway. (#60788) Thanks @openperf.
- Exec/heartbeat: use the canonical `exec-event` wake reason for `notifyOnExit` so background exec completions still trigger follow-up turns when `HEARTBEAT.md` is empty or comments-only. (#41479) Thanks @rstar327.
- Heartbeat: skip wake delivery when the target session lane is already busy so the pending event is retried instead of getting drained too early. (#40526) Thanks @lucky7323.
@@ -5390,11 +5789,11 @@ Docs: https://docs.openclaw.ai
- Plugins/OpenAI: tune the OpenAI prompt overlay for live-chat cadence so GPT replies stay shorter, more human, and less wall-of-text by default. Thanks @vincentkoc.
- Providers/compat: stop forcing OpenAI-only defaults on proxy and custom OpenAI-compatible routes, preserve native vendor-specific reasoning/tool/streaming behavior across Anthropic-compatible, Moonshot, Mistral, ModelStudio, OpenRouter, xAI, and Z.ai endpoints, and route GitHub Copilot Claude models through Anthropic Messages instead of OpenAI Responses. Thanks @vincentkoc.
- Providers/GitHub Copilot: send IDE identity headers on runtime model requests and GitHub token exchange so IDE-authenticated Copilot runs stop failing with missing `Editor-Version`. (#60641) Thanks @VACInc and @vincentkoc.
- Providers/OpenRouter failover: classify `403 Key limit exceeded` spending-limit responses as billing so model fallback continues instead of stopping on generic auth. (#59892) Thanks @rockcent.
- Providers/OpenRouter failover: classify `403 “Key limit exceeded”` spending-limit responses as billing so model fallback continues instead of stopping on generic auth. (#59892) Thanks @rockcent.
- Providers/Anthropic: keep `claude-cli/*` auth on live Claude CLI credentials at runtime, avoid persisting stale bearer-token profiles, and suppress macOS Keychain prompts during non-interactive Claude CLI setup. (#61234) Thanks @darkamenosa.
- Providers/Anthropic: when Claude CLI auth becomes the default, write a real `claude-cli` auth profile so local and gateway agent runs can use Claude CLI immediately without missing-API-key failures. Thanks @vincentkoc.
- Memory/dreaming: make Dreams config reads and writes respect the selected memory slot plugin (including `doctor.memory.status` and Control UI fallback state) instead of always targeting `memory-core`. (#62275) Thanks @SnowSky1.
- Providers/Anthropic Vertex: honor `cacheRetention: long` with the real 1-hour prompt-cache TTL on Vertex AI endpoints, and default `anthropic-vertex` cache retention like direct Anthropic. (#60888) Thanks @affsantos.
- Providers/Anthropic Vertex: honor `cacheRetention: “long”` with the real 1-hour prompt-cache TTL on Vertex AI endpoints, and default `anthropic-vertex` cache retention like direct Anthropic. (#60888) Thanks @affsantos.
- Agents/Anthropic: preserve native `toolu_*` replay ids on direct Anthropic and Anthropic Vertex paths so cache-sensitive history stops rewriting known-valid Anthropic tool-use ids. (#52612) Thanks @vincentkoc.
- Providers/Google: add model-level `cacheRetention` support for direct Gemini system prompts by creating, reusing, and refreshing `cachedContents` automatically on Google AI Studio runs. (#51372) Thanks @rafaelmariano-glitch.
- Google Gemini CLI auth: detect bundled npm installs by scanning packaged bundle files for the Gemini OAuth client config, so `npm install -g @google/gemini-cli` layouts work again. (#60486) Thanks @wzfmini01.
@@ -5408,7 +5807,7 @@ Docs: https://docs.openclaw.ai
- Amazon Bedrock/aws-sdk auth: stop injecting the fake `AWS_PROFILE` apiKey marker when no AWS auth env vars exist, so instance-role and other default-chain setups keep working without poisoning provider config. (#61194) Thanks @wirjo.
- Agents/Kimi tool-call repair: preserve tool arguments that were already present on streamed tool calls when later malformed deltas fail reevaluation, while still dropping stale repair-only state before `toolcall_end`. Thanks @vincentkoc.
- Plugins/Kimi Coding: parse tagged tool calls and keep Anthropic-native tool payloads so Kimi coding endpoints execute tools instead of echoing raw markup. (#60051, #60391) Thanks @obviyus and @Eric-Guo.
- Media understanding: auto-register image-capable config providers for vision routing, so custom GLM-style provider ids with image models stop failing with no media-understanding provider registered. (#51418) Thanks @xydt-610.
- Media understanding: auto-register image-capable config providers for vision routing, so custom GLM-style provider ids with image models stop failing with “no media-understanding provider registered”. (#51418) Thanks @xydt-610.
- Plugins/media understanding: enable bundled Groq and Deepgram providers by default so configured transcription models work without extra plugin activation config. (#59982) Thanks @yxjsxy.
- MiniMax/pricing: keep bundled MiniMax highspeed pricing distinct in provider catalogs and preserve the lower M2.5 cache-read pricing when onboarding older MiniMax models. (#54214) Thanks @octo-patch.
- MiniMax: advertise image input on bundled `MiniMax-M2.7` and `MiniMax-M2.7-highspeed` model definitions so image-capable flows can route through the M2.7 family correctly. (#54843) Thanks @MerlinMiao88888888.
@@ -5582,7 +5981,7 @@ Docs: https://docs.openclaw.ai
- Matrix/plugin: emit spec-compliant `m.mentions` metadata across text sends, media captions, edits, poll fallback text, and action-driven edits so Matrix mentions notify reliably in clients like Element. (#59323) Thanks @gumadeiras.
- Diffs: add plugin-owned `viewerBaseUrl` so viewer links can use a stable proxy/public origin without passing `baseUrl` on every tool call. (#59341) Related #59227. Thanks @gumadeiras.
- Agents/compaction: resolve `agents.defaults.compaction.model` consistently for manual `/compact` and other context-engine compaction paths, so engine-owned compaction uses the configured override model across runtime entrypoints. (#56710) Thanks @oliviareid-svg.
- Agents/compaction: add `agents.defaults.compaction.notifyUser` so the `🧹 Compacting context...` start notice is opt-in instead of always being shown. (#54251) Thanks @oguricap0327.
- Agents/compaction: add `agents.defaults.compaction.notifyUser` so the `🧹 Compacting context...` start notice is opt-in instead of always being shown. (#54251) Thanks @oguricap0327.
- WhatsApp/reactions: add `reactionLevel` guidance for agent reactions. Thanks @mcaxtr.
- Exec approvals/channels: auto-enable DM-first native chat approvals when supported channels can infer approvers from existing owner config, while keeping channel fanout explicit and clarifying forwarding versus native approval client config.
- Android/assistant: auto-send Google Assistant App Actions prompts once chat is healthy and idle, while keeping bare assistant launches as open-only. (#59721) Thanks @obviyus.
@@ -5642,7 +6041,7 @@ Docs: https://docs.openclaw.ai
- Exec/env: block additional host environment override pivots for package roots, language runtimes, compiler include paths, and credential/config locations so request-scoped exec cannot redirect trusted toolchains or config lookups. (#59233) Thanks @drobison00.
- Dotenv/workspace overrides: block workspace `.env` files from overriding `OPENCLAW_PINNED_PYTHON` and `OPENCLAW_PINNED_WRITE_PYTHON` so trusted helper interpreters cannot be redirected by repo-local env injection. (#58473) Thanks @eleqtrizit.
- Plugins/install: accept JSON5 syntax in `openclaw.plugin.json` and bundle `plugin.json` manifests during install/validation, so third-party plugins with trailing commas, comments, or unquoted keys no longer fail to install. (#59084) Thanks @singleGanghood.
- Telegram/exec approvals: rewrite shared `/approve allow-always` callback payloads to `/approve always` before Telegram button rendering so plugin approval IDs still fit Telegram's `callback_data` limit and keep the Allow Always action visible. (#59217) Thanks @jameslcowan.
- Telegram/exec approvals: rewrite shared `/approve … allow-always` callback payloads to `/approve … always` before Telegram button rendering so plugin approval IDs still fit Telegram's `callback_data` limit and keep the Allow Always action visible. (#59217) Thanks @jameslcowan.
- Cron/exec timeouts: surface timed-out `exec` and `bash` failures in isolated cron runs even when `verbose: off`, including custom session-target cron jobs, so scheduled runs stop failing silently. (#58247) Thanks @skainguyen1412.
- Telegram/exec approvals: fall back to the origin session key for async approval followups and keep resume-failure status delivery sanitized so Telegram followups still land without leaking raw exec metadata. (#59351) Thanks @seonang.
- Node-host/exec approvals: bind `pnpm dlx` invocations through the approval planner's mutable-script path so the effective runtime command is resolved for approval instead of being left unbound. (#58374) Thanks @jacobtomlinson.
@@ -5735,7 +6134,7 @@ Docs: https://docs.openclaw.ai
- Pi/Codex: add native Codex web search support for embedded Pi runs, including config/docs/wizard coverage and managed-tool suppression when native Codex search is active. (#46579) Thanks @Evizero.
- Slack/exec approvals: add native Slack approval routing and approver authorization so exec approval prompts can stay in Slack instead of falling back to the Web UI or terminal. Thanks @vincentkoc.
- TTS: Add structured provider diagnostics and fallback attempt analytics. (#57954) Thanks @joshavant.
- WhatsApp/reactions: agents can now react with emoji on incoming WhatsApp messages, enabling more natural conversational interactions like acknowledging a photo with ❤️ instead of typing a reply. Thanks @mcaxtr.
- WhatsApp/reactions: agents can now react with emoji on incoming WhatsApp messages, enabling more natural conversational interactions like acknowledging a photo with ❤️ instead of typing a reply. Thanks @mcaxtr.
- Agents/BTW: force `/btw` side questions to disable provider reasoning so Anthropic adaptive-thinking sessions stop failing with `No BTW response generated`. Fixes #55376. Thanks @Catteres and @vincentkoc.
- CLI/onboarding: reset the remote gateway URL prompt to the safe loopback default after declining a discovered endpoint, so onboarding does not keep a previously rejected remote URL. (#57828).
- Agents/exec defaults: honor per-agent `tools.exec` defaults when no inline directive or session override is present, so configured exec host, security, ask, and node settings actually apply. (#57689).
@@ -8193,7 +8592,7 @@ Docs: https://docs.openclaw.ai
- Slack/Threading: sessions: keep parent-session forking and thread-history context active beyond first turn by removing first-turn-only gates in session init, thread-history fetch, and reply prompt context injection. (#23843, #23090) Thanks @vincentkoc and @Taskle.
- Slack/Threading: respect `replyToMode` when Slack auto-populates top-level `thread_ts`, and ignore inline `replyToId` directive tags when `replyToMode` is `off` so thread forcing stays disabled unless explicitly configured. (#23839, #23320, #23513) Thanks @vincentkoc and @dorukardahan.
- Slack/Extension: forward `message read` `threadId` to `readMessages` and use delivery-context `threadId` as outbound `thread_ts` fallback so extension replies/reads stay in the correct Slack thread. (#22216, #22485, #23836) Thanks @vincentkoc, @lan17 and @dorukardahan.
- Slack/Upload: resolve bare user IDs (U-prefix) to DM channel IDs via `conversations.open`, and replace `files.uploadV2` with Slack's external 3-step upload flow (`files.getUploadURLExternal` presigned upload POST `files.completeUploadExternal`) to avoid `missing_scope`/`invalid_arguments` upload failures in DM and threaded media replies (#57018). Thanks @hydro13.
- Slack/Upload: resolve bare user IDs (U-prefix) to DM channel IDs via `conversations.open`, and replace `files.uploadV2` with Slack's external 3-step upload flow (`files.getUploadURLExternal` → presigned upload POST → `files.completeUploadExternal`) to avoid `missing_scope`/`invalid_arguments` upload failures in DM and threaded media replies (#57018). Thanks @hydro13.
- Webchat/Chat: apply assistant `final` payload messages directly to chat state so sent turns render without waiting for a full history refresh cycle. (#14928) Thanks @BradGroux.
- Webchat/Chat: for out-of-band final events (for example tool-call side runs), append provided final assistant payloads directly instead of forcing a transient history reset. (#11139) Thanks @AkshayNavle.
- Webchat/Performance: reload `chat.history` after final events only when the final payload lacks a renderable assistant message, avoiding expensive full-history refreshes on normal turns. (#20588) Thanks @amzzzzzzz.
@@ -8347,7 +8746,7 @@ Docs: https://docs.openclaw.ai
- Agents/Transcripts: validate assistant tool-call names (syntax/length + registered tool allowlist) before transcript persistence and during replay sanitization so malformed failover tool names no longer poison sessions with repeated provider HTTP 400 errors. (#23324) Thanks @johnsantry.
- Agents/Mistral: sanitize tool-call IDs in the embedded agent loop and generate strict provider-safe pending tool-call IDs, preventing Mistral strict9 `HTTP 400` failures on tool continuations. (#23698) Thanks @echoVic.
- Agents/Compaction: strip stale assistant usage snapshots from pre-compaction turns when replaying history after a compaction summary so context-token estimation no longer reuses pre-compaction totals and immediately re-triggers destructive follow-up compactions. (#19127) Thanks @tedwatson.
- Agents/Replies: emit a default completion acknowledgement (` Done.`) only for direct/private tool-only completions with no final assistant text, while suppressing synthetic acknowledgements for channel/group sessions and runs that already delivered output via messaging tools. (#22834) Thanks @Oldshue.
- Agents/Replies: emit a default completion acknowledgement (`✅ Done.`) only for direct/private tool-only completions with no final assistant text, while suppressing synthetic acknowledgements for channel/group sessions and runs that already delivered output via messaging tools. (#22834) Thanks @Oldshue.
- Agents/Subagents: honor `tools.subagents.tools.alsoAllow` and explicit subagent `allow` entries when resolving built-in subagent deny defaults, so explicitly granted tools (for example `sessions_send`) are no longer blocked unless re-denied in `tools.subagents.tools.deny`. (#23359) Thanks @goren-beehero.
- Agents/Subagents: make announce call timeouts configurable via `agents.defaults.subagents.announceTimeoutMs` and restore a 60s default to prevent false timeout failures on slower announce paths. (#22719) Thanks @Valadon.
- Agents/Diagnostics: include resolved lifecycle error text in `embedded run agent end` warnings so UI/TUI "Connection error" runs expose actionable provider failure reasons in gateway logs. (#23054) Thanks @Raize.
@@ -9191,7 +9590,7 @@ Docs: https://docs.openclaw.ai
- Sessions/Gateway: harden transcript path resolution and reject unsafe session IDs/file paths so session operations stay within agent sessions directories. Thanks @akhmittra.
- Sessions: preserve `verboseLevel`, `thinkingLevel`/`reasoningLevel`, and `ttsAuto` overrides across `/new` and `/reset` session resets. (#10787) Thanks @mcaxtr.
- Gateway: raise WS payload/buffer limits so 5,000,000-byte image attachments work reliably. (#14486) Thanks @0xRaini.
- Logging/CLI: use local timezone timestamps for console prefixing, and include `±HH:MM` offsets when using `openclaw logs --local-time` to avoid ambiguity. (#14771) Thanks @0xRaini.
- Logging/CLI: use local timezone timestamps for console prefixing, and include `±HH:MM` offsets when using `openclaw logs --local-time` to avoid ambiguity. (#14771) Thanks @0xRaini.
- Gateway: drain active turns before restart to prevent message loss. (#13931) Thanks @0xRaini.
- Gateway: auto-generate auth token during install to prevent launchd restart loops. (#13813) Thanks @cathrynlavery.
- Gateway: prevent `undefined`/missing token in auth config. (#13809) Thanks @asklee-klawd.
@@ -10297,7 +10696,7 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
### Changes
- UI/Apps: move channel/config settings to schema-driven forms and rename Connections Channels. (#1040) - thanks @thewilloftheshadow.
- UI/Apps: move channel/config settings to schema-driven forms and rename Connections → Channels. (#1040) - thanks @thewilloftheshadow.
- CLI: set process titles to `openclaw-<command>` for clearer process listings.
- CLI/macOS: sync remote SSH target/identity to config and let `gateway status` auto-infer SSH targets (ssh-config aware).
- Telegram: scope inline buttons with allowlist default + callback gating in DMs/groups.
@@ -10323,7 +10722,7 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
- Auth/Status: keep auth profiles sticky per session (rotate on compaction/new), surface provider usage headers in `/status` and `openclaw models status`, and update docs.
- CLI: add `--json` output for `openclaw daemon` lifecycle/install commands.
- Memory: make `node-llama-cpp` an optional dependency (avoid Node 25 install failures) and improve local-embeddings fallback/errors.
- Browser: add `snapshot refs=aria` (Playwright aria-ref ids) for self-resolving refs across `snapshot` `act`.
- Browser: add `snapshot refs=aria` (Playwright aria-ref ids) for self-resolving refs across `snapshot` → `act`.
- Browser: `profile="chrome"` now defaults to host control and returns clearer "attach a tab" errors (#57018). Thanks @hydro13.
- Browser: prefer stable Chrome for auto-detect, with Brave/Edge fallbacks and updated docs. (#983) - thanks @cpojer.
- Browser: increase remote CDP reachability timeouts + add `remoteCdpTimeoutMs`/`remoteCdpHandshakeTimeoutMs`.
@@ -10360,7 +10759,7 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
- Fix: suppress WhatsApp pairing replies for historical catch-up DMs on initial link. (#904).
- Browser: extension mode recovers when only one tab is attached (stale targetId fallback).
- Browser: fix `tab not found` for extension relay snapshots/actions when Playwright blocks `newCDPSession` (use the single available Page).
- Browser: upgrade `ws` `wss` when remote CDP uses `https` (fixes Browserless handshake).
- Browser: upgrade `ws` → `wss` when remote CDP uses `https` (fixes Browserless handshake).
- Telegram: skip `message_thread_id=1` for General topic sends while keeping typing indicators. (#848) - thanks @azade-c.
- Fix: sanitize user-facing error text + strip `<final>` tags across reply pipelines. (#975) - thanks @ThomsenDrake.
- Fix: normalize pairing CLI aliases, allow extension channels, and harden Zalo webhook payload parsing. (#991) - thanks @longmaba.
@@ -10696,7 +11095,7 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
- WhatsApp: fix group reactions by preserving message IDs and sender JIDs in history; normalize participant phone numbers to JIDs in outbound reactions. (#640) - thanks @mcinteerj.
- WhatsApp: expose group participant IDs to the model so reactions can target the right sender.
- Cron: `wakeMode: "now"` waits for heartbeat completion (and retries when the main lane is busy). (#666) - thanks @roshanasingh4.
- Agents/OpenAI: fix Responses tool-only follow-up turn handling (avoid standalone `reasoning` items that trigger 400 "required following item") and replay reasoning items in Responses/Codex Responses history for tool-call-only turns (#57018). Thanks @hydro13.
- Agents/OpenAI: fix Responses tool-only → follow-up turn handling (avoid standalone `reasoning` items that trigger 400 "required following item") and replay reasoning items in Responses/Codex Responses history for tool-call-only turns (#57018). Thanks @hydro13.
- Sandbox: add `openclaw sandbox explain` (effective policy inspector + fix-it keys); improve "sandbox jail" tool-policy/elevated errors with actionable config key paths; link to docs (#57018). Thanks @hydro13.
- Hooks/Gmail: keep Tailscale serve path at `/` while preserving the public path. (#668) - thanks @antons.
- Hooks/Gmail: allow Tailscale target URLs to preserve internal serve paths.

View File

@@ -293,4 +293,4 @@ USER node
HEALTHCHECK --interval=3m --timeout=10s --start-period=15s --retries=3 \
CMD node -e "fetch('http://127.0.0.1:18789/healthz').then((r)=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
ENTRYPOINT ["tini", "-s", "--"]
CMD ["node", "openclaw.mjs", "gateway", "--allow-unconfigured"]
CMD ["node", "openclaw.mjs", "gateway"]

View File

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

View File

@@ -12,6 +12,7 @@ import ai.openclaw.app.gateway.GatewayEndpoint
import ai.openclaw.app.gateway.GatewaySession
import ai.openclaw.app.gateway.GatewayTlsProbeFailure
import ai.openclaw.app.gateway.GatewayTlsProbeResult
import ai.openclaw.app.gateway.normalizeGatewayTlsFingerprint
import ai.openclaw.app.gateway.probeGatewayTlsFingerprint
import ai.openclaw.app.node.A2UIHandler
import ai.openclaw.app.node.CalendarHandler
@@ -248,6 +249,7 @@ class NodeRuntime(
val endpoint: GatewayEndpoint,
val fingerprintSha256: String,
val auth: GatewayConnectAuth,
val previousFingerprintSha256: String? = null,
)
private val _isConnected = MutableStateFlow(false)
@@ -260,6 +262,7 @@ class NodeRuntime(
private val _pendingGatewayTrust = MutableStateFlow<GatewayTrustPrompt?>(null)
val pendingGatewayTrust: StateFlow<GatewayTrustPrompt?> = _pendingGatewayTrust.asStateFlow()
private val connectAttemptSeq = AtomicLong(0)
private fun resolveNodeMainSessionKey(agentId: String? = gatewayDefaultAgentId): String {
val deviceId = identityStore.loadOrCreate().deviceId
@@ -1112,23 +1115,58 @@ class NodeRuntime(
endpoint: GatewayEndpoint,
auth: GatewayConnectAuth,
) {
val connectAttemptId = connectAttemptSeq.incrementAndGet()
_pendingGatewayTrust.value = null
val tls = connectionManager.resolveTlsParams(endpoint)
if (tls?.required == true && tls.expectedFingerprint.isNullOrBlank()) {
// First-time TLS: capture fingerprint, ask user to verify out-of-band, then store and connect.
if (tls?.required == true) {
val expectedFingerprint =
tls.expectedFingerprint
?.let(::normalizeGatewayTlsFingerprint)
?.takeIf { it.isNotBlank() }
_statusText.value = "Verify gateway TLS fingerprint…"
scope.launch {
val tlsProbe = tlsFingerprintProbe(endpoint.host, endpoint.port)
if (!isCurrentConnectAttempt(connectAttemptId)) return@launch
val fp =
tlsProbe.fingerprintSha256 ?: run {
_statusText.value = gatewayTlsProbeFailureMessage(tlsProbe.failure)
if (expectedFingerprint == null) {
_statusText.value = gatewayTlsProbeFailureMessage(tlsProbe.failure)
} else {
connectAfterTlsCheck(endpoint = endpoint, auth = auth, connectAttemptId = connectAttemptId)
}
return@launch
}
_pendingGatewayTrust.value =
GatewayTrustPrompt(endpoint = endpoint, fingerprintSha256 = fp, auth = auth)
val observedFingerprint =
normalizeGatewayTlsFingerprint(fp)
.takeIf { it.isNotBlank() }
?: fp
val previousFingerprint = expectedFingerprint?.takeUnless { it == observedFingerprint }
if (expectedFingerprint == null || previousFingerprint != null) {
_pendingGatewayTrust.value =
GatewayTrustPrompt(
endpoint = endpoint,
fingerprintSha256 = observedFingerprint,
auth = auth,
previousFingerprintSha256 = previousFingerprint,
)
return@launch
}
connectAfterTlsCheck(endpoint = endpoint, auth = auth, connectAttemptId = connectAttemptId)
}
return
}
connectAfterTlsCheck(endpoint = endpoint, auth = auth, connectAttemptId = connectAttemptId)
}
private fun isCurrentConnectAttempt(connectAttemptId: Long): Boolean = connectAttemptSeq.get() == connectAttemptId
private fun connectAfterTlsCheck(
endpoint: GatewayEndpoint,
auth: GatewayConnectAuth,
connectAttemptId: Long,
) {
if (!isCurrentConnectAttempt(connectAttemptId)) return
connectedEndpoint = endpoint
operatorStatusText = "Connecting…"
nodeStatusText = "Connecting…"
@@ -1221,6 +1259,7 @@ class NodeRuntime(
}
fun disconnect() {
connectAttemptSeq.incrementAndGet()
stopActiveVoiceSession()
connectedEndpoint = null
activeGatewayAuth = null

View File

@@ -53,7 +53,10 @@ fun buildGatewayTlsConfig(
onStore: ((String) -> Unit)? = null,
): GatewayTlsConfig? {
if (params == null) return null
val expected = params.expectedFingerprint?.let(::normalizeFingerprint)
val expected =
params.expectedFingerprint
?.let(::normalizeGatewayTlsFingerprint)
?.takeIf { it.isNotBlank() }
val defaultTrust = defaultTrustManager()
@SuppressLint("CustomX509TrustManager")
@@ -200,7 +203,7 @@ private fun sha256Hex(data: ByteArray): String {
return out.toString()
}
private fun normalizeFingerprint(raw: String): String {
fun normalizeGatewayTlsFingerprint(raw: String): String {
val stripped =
raw
.trim()

View File

@@ -100,8 +100,14 @@ fun ConnectTabScreen(viewModel: MainViewModel) {
containerColor = mobileCardSurface,
title = { Text("Trust this gateway?", style = mobileHeadline, color = mobileText) },
text = {
val message =
if (prompt.previousFingerprintSha256.isNullOrBlank()) {
"First-time TLS connection.\n\nVerify this SHA-256 fingerprint before trusting:\n${prompt.fingerprintSha256}"
} else {
"The gateway TLS certificate changed. Only continue if you expected this.\n\nOld SHA-256 fingerprint:\n${prompt.previousFingerprintSha256}\n\nNew SHA-256 fingerprint:\n${prompt.fingerprintSha256}"
}
Text(
"First-time TLS connection.\n\nVerify this SHA-256 fingerprint before trusting:\n${prompt.fingerprintSha256}",
message,
style = mobileCallout,
color = mobileText,
)

View File

@@ -497,8 +497,14 @@ fun OnboardingFlow(
containerColor = onboardingSurface,
title = { Text("Trust this gateway?", style = onboardingHeadlineStyle, color = onboardingText) },
text = {
val message =
if (prompt.previousFingerprintSha256.isNullOrBlank()) {
"First-time TLS connection.\n\nVerify this SHA-256 fingerprint before trusting:\n${prompt.fingerprintSha256}"
} else {
"The gateway TLS certificate changed. Only continue if you expected this.\n\nOld SHA-256 fingerprint:\n${prompt.previousFingerprintSha256}\n\nNew SHA-256 fingerprint:\n${prompt.fingerprintSha256}"
}
Text(
"First-time TLS connection.\n\nVerify this SHA-256 fingerprint before trusting:\n${prompt.fingerprintSha256}",
message,
style = onboardingCalloutStyle,
color = onboardingText,
)

View File

@@ -30,13 +30,16 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.LinkAnnotation
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextLinkStyles
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.withLink
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@@ -499,19 +502,12 @@ private fun AnnotatedString.Builder.appendInlineNode(
}
}
is Link -> {
withStyle(
SpanStyle(
color = linkColor,
textDecoration = TextDecoration.Underline,
),
) {
appendInlineNode(
current.firstChild,
inlineCodeBg = inlineCodeBg,
inlineCodeColor = inlineCodeColor,
linkColor = linkColor,
)
}
appendLinkNode(
link = current,
inlineCodeBg = inlineCodeBg,
inlineCodeColor = inlineCodeColor,
linkColor = linkColor,
)
}
is MarkdownImage -> {
val alt = buildPlainText(current.firstChild)
@@ -527,13 +523,69 @@ private fun AnnotatedString.Builder.appendInlineNode(
}
}
else -> {
appendInlineNode(current.firstChild, inlineCodeBg = inlineCodeBg, inlineCodeColor = inlineCodeColor, linkColor = linkColor)
appendInlineNode(
current.firstChild,
inlineCodeBg = inlineCodeBg,
inlineCodeColor = inlineCodeColor,
linkColor = linkColor,
)
}
}
current = current.next
}
}
private fun AnnotatedString.Builder.appendLinkNode(
link: Link,
inlineCodeBg: Color,
inlineCodeColor: Color,
linkColor: Color,
) {
val destination = link.destination?.trim().orEmpty()
val linkStyle =
SpanStyle(
color = linkColor,
textDecoration = TextDecoration.Underline,
)
if (destination.isEmpty()) {
withStyle(linkStyle) {
appendInlineNode(
link.firstChild,
inlineCodeBg = inlineCodeBg,
inlineCodeColor = inlineCodeColor,
linkColor = linkColor,
)
}
return
}
withLink(LinkAnnotation.Url(url = destination, styles = TextLinkStyles(style = linkStyle))) {
appendInlineNode(
link.firstChild,
inlineCodeBg = inlineCodeBg,
inlineCodeColor = inlineCodeColor,
linkColor = linkColor,
)
}
}
internal fun buildChatInlineMarkdown(
text: String,
linkColor: Color = Color.Blue,
): AnnotatedString {
val document = markdownParser.parse(text) as Document
val paragraph = document.firstChild as? Paragraph ?: return AnnotatedString("")
return buildInlineMarkdown(
paragraph.firstChild,
InlineStyles(
inlineCodeBg = Color.Transparent,
inlineCodeColor = Color.Unspecified,
linkColor = linkColor,
baseCallout = TextStyle.Default,
),
)
}
private fun buildPlainText(start: Node?): String {
val sb = StringBuilder()
var node = start

View File

@@ -10,6 +10,7 @@ import ai.openclaw.app.node.InvokeDispatcher
import ai.openclaw.app.protocol.OpenClawTalkCommand
import ai.openclaw.app.voice.TalkModeManager
import android.Manifest
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
@@ -153,7 +154,7 @@ class GatewayBootstrapAuthTest {
NodeRuntime(
app,
prefs,
tlsFingerprintProbe = { _, _ -> GatewayTlsProbeResult(fingerprintSha256 = "fp-1") },
tlsFingerprintProbe = { _, _ -> GatewayTlsProbeResult(fingerprintSha256 = "fp:1") },
)
val endpoint = GatewayEndpoint.manual(host = "gateway.example", port = 18789)
val explicitAuth =
@@ -169,11 +170,93 @@ class GatewayBootstrapAuthTest {
runtime.acceptGatewayTrustPrompt()
assertEquals("fp-1", prefs.loadGatewayTlsFingerprint(endpoint.stableId))
assertEquals("setup-bootstrap-token", desiredBootstrapToken(runtime, "nodeSession"))
assertEquals("f1", prefs.loadGatewayTlsFingerprint(endpoint.stableId))
assertEquals("setup-bootstrap-token", waitForDesiredBootstrapToken(runtime, "nodeSession"))
assertNull(desiredBootstrapToken(runtime, "operatorSession"))
}
@Test
fun connect_promptsBeforeReplacingChangedTlsFingerprint() =
runBlocking {
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 endpoint = GatewayEndpoint.manual(host = "gateway.example", port = 18789)
prefs.saveGatewayTlsFingerprint(endpoint.stableId, "sha256:aa:aa:aa:aa")
val runtime =
NodeRuntime(
app,
prefs,
tlsFingerprintProbe = { _, _ -> GatewayTlsProbeResult(fingerprintSha256 = "sha256:bb:bb:bb:bb") },
)
runtime.connect(
endpoint,
NodeRuntime.GatewayConnectAuth(token = "shared-token", bootstrapToken = null, password = null),
)
val prompt = waitForGatewayTrustPrompt(runtime)
assertEquals("aaaaaaaa", prompt.previousFingerprintSha256)
assertEquals("bbbbbbbb", prompt.fingerprintSha256)
assertEquals("sha256:aa:aa:aa:aa", prefs.loadGatewayTlsFingerprint(endpoint.stableId))
runtime.declineGatewayTrustPrompt()
assertEquals("sha256:aa:aa:aa:aa", prefs.loadGatewayTlsFingerprint(endpoint.stableId))
runtime.connect(
endpoint,
NodeRuntime.GatewayConnectAuth(token = "shared-token", bootstrapToken = null, password = null),
)
waitForGatewayTrustPrompt(runtime)
runtime.acceptGatewayTrustPrompt()
assertEquals("bbbbbbbb", prefs.loadGatewayTlsFingerprint(endpoint.stableId))
}
@Test
fun connect_ignoresStaleTlsProbeAfterDisconnect() =
runBlocking {
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 endpoint = GatewayEndpoint.manual(host = "gateway.example", port = 18789)
prefs.saveGatewayTlsFingerprint(endpoint.stableId, "aaaaaaaa")
val probeStarted = CompletableDeferred<Unit>()
val probeResult = CompletableDeferred<GatewayTlsProbeResult>()
val runtime =
NodeRuntime(
app,
prefs,
tlsFingerprintProbe = { _, _ ->
probeStarted.complete(Unit)
probeResult.await()
},
)
runtime.connect(
endpoint,
NodeRuntime.GatewayConnectAuth(token = "shared-token", bootstrapToken = null, password = null),
)
probeStarted.await()
runtime.disconnect()
probeResult.complete(GatewayTlsProbeResult(fingerprintSha256 = "aaaaaaaa"))
Thread.sleep(100)
assertNull(runtime.pendingGatewayTrust.value)
assertNull(desiredBootstrapToken(runtime, "nodeSession"))
assertEquals("aaaaaaaa", prefs.loadGatewayTlsFingerprint(endpoint.stableId))
}
@Test
fun connect_showsSecureEndpointGuidanceWhenTlsProbeFails() {
val app = RuntimeEnvironment.getApplication()
@@ -269,6 +352,21 @@ class GatewayBootstrapAuthTest {
return readField(desired, "bootstrapToken")
}
private fun waitForDesiredBootstrapToken(
runtime: NodeRuntime,
sessionFieldName: String,
): String {
var lastObserved: String? = null
repeat(50) {
desiredBootstrapToken(runtime, sessionFieldName)?.let { token ->
lastObserved = token
return token
}
Thread.sleep(10)
}
error("Expected desired bootstrap token for $sessionFieldName; last observed=$lastObserved")
}
private fun <T> readField(
target: Any,
name: String,

View File

@@ -0,0 +1,42 @@
package ai.openclaw.app.ui.chat
import androidx.compose.ui.text.LinkAnnotation
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
class ChatMarkdownTest {
@Test
fun bareUrlsCarryClickableUrlAnnotations() {
val url = "https://www.amazon.it/GAZEBO-CANOPY-ACCIAIO-BIANCO-IMPERMEABILE/dp/B01G5R9FCK"
val annotated = buildChatInlineMarkdown("Open $url")
assertEquals("Open $url", annotated.text)
val links = annotated.getLinkAnnotations(0, annotated.length)
assertEquals(1, links.size)
assertEquals(5, links.single().start)
assertEquals(5 + url.length, links.single().end)
assertEquals(url, (links.single().item as LinkAnnotation.Url).url)
}
@Test
fun markdownLinksUseLabelTextAndDestinationUrl() {
val annotated = buildChatInlineMarkdown("Open [docs](https://docs.openclaw.ai/help/testing) now")
assertEquals("Open docs now", annotated.text)
val links = annotated.getLinkAnnotations(0, annotated.length)
assertEquals(1, links.size)
assertEquals(5, links.single().start)
assertEquals(9, links.single().end)
assertEquals("https://docs.openclaw.ai/help/testing", (links.single().item as LinkAnnotation.Url).url)
}
@Test
fun plainTextDoesNotAddLinkAnnotations() {
val annotated = buildChatInlineMarkdown("No link here")
assertEquals("No link here", annotated.text)
assertTrue(annotated.getLinkAnnotations(0, annotated.length).isEmpty())
}
}

View File

@@ -1,5 +1,9 @@
# OpenClaw iOS Changelog
## 2026.5.17 - 2026-05-17
Maintenance update for the current OpenClaw release.
## 2026.5.12 - 2026-05-12
Maintenance update for the current OpenClaw beta 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.16
OPENCLAW_MARKETING_VERSION = 2026.5.16
OPENCLAW_IOS_VERSION = 2026.5.17
OPENCLAW_MARKETING_VERSION = 2026.5.17
OPENCLAW_BUILD_VERSION = 1
#include? "../build/Version.xcconfig"

View File

@@ -1 +1 @@
Maintenance update for the current OpenClaw beta release.
Maintenance update for the current OpenClaw release.

View File

@@ -1,3 +1,3 @@
{
"version": "2026.5.16"
"version": "2026.5.17"
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

View File

@@ -81,6 +81,7 @@ let package = Package(
dependencies: [
"OpenClawIPC",
"OpenClaw",
"OpenClawMacCLI",
"OpenClawDiscovery",
.product(name: "OpenClawProtocol", package: "OpenClawKit"),
.product(name: "SwabbleKit", package: "swabble"),

View File

@@ -318,7 +318,12 @@ final class AppState {
self.iconAnimationsEnabled = true
UserDefaults.standard.set(true, forKey: iconAnimationsEnabledKey)
}
self.showDockIcon = UserDefaults.standard.bool(forKey: showDockIconKey)
if let storedShowDockIcon = UserDefaults.standard.object(forKey: showDockIconKey) as? Bool {
self.showDockIcon = storedShowDockIcon
} else {
self.showDockIcon = true
UserDefaults.standard.set(true, forKey: showDockIconKey)
}
self.voiceWakeMicID = UserDefaults.standard.string(forKey: voiceWakeMicKey) ?? ""
self.voiceWakeMicName = UserDefaults.standard.string(forKey: voiceWakeMicNameKey) ?? ""
self.voiceWakeLocaleID = UserDefaults.standard.string(forKey: voiceWakeLocaleKey) ?? Locale.current.identifier
@@ -358,19 +363,29 @@ final class AppState {
}
let configRoot = OpenClawConfigFile.loadDict()
let configRemoteUrl = GatewayRemoteConfig.resolveUrlString(root: configRoot)
let configRemoteToken = GatewayRemoteConfig.resolveTokenValue(root: configRoot)
let configRemoteTransport = GatewayRemoteConfig.resolveTransport(root: configRoot)
let configRemoteResolution = GatewayRemoteConfig.resolveTransportResolution(root: configRoot)
let configRemoteTransport = configRemoteResolution.transport
let configRemoteUrl = configRemoteResolution.directURL?.absoluteString
?? GatewayRemoteConfig.resolveUrlString(root: configRoot)
let resolvedConnectionMode = ConnectionModeResolver.resolve(root: configRoot).mode
self.remoteTransport = configRemoteTransport
self.connectionMode = resolvedConnectionMode
let configRemote = (configRoot["gateway"] as? [String: Any])?["remote"] as? [String: Any]
let configRemoteTarget = (configRemote?["sshTarget"] as? String)?
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
let storedRemoteTarget = UserDefaults.standard.string(forKey: remoteTargetKey) ?? ""
if resolvedConnectionMode == .remote,
configRemoteTransport != .direct,
storedRemoteTarget.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty,
let host = AppState.remoteHost(from: configRemoteUrl),
!LoopbackHost.isLoopbackHost(host)
!configRemoteTarget.isEmpty,
storedRemoteTarget.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
{
self.remoteTarget = configRemoteTarget
} else if resolvedConnectionMode == .remote,
configRemoteTransport != .direct,
storedRemoteTarget.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty,
let host = AppState.remoteHost(from: configRemoteUrl),
!LoopbackHost.isLoopbackHost(host)
{
self.remoteTarget = "\(NSUserName())@\(host)"
} else {
@@ -380,9 +395,11 @@ final class AppState {
self.remoteToken = configRemoteToken.textFieldValue
self.remoteTokenDirty = false
self.remoteTokenUnsupported = configRemoteToken.isUnsupportedNonString
self.remoteIdentity = UserDefaults.standard.string(forKey: remoteIdentityKey) ?? ""
self.remoteProjectRoot = UserDefaults.standard.string(forKey: remoteProjectRootKey) ?? ""
self.remoteCliPath = UserDefaults.standard.string(forKey: remoteCliPathKey) ?? ""
self.remoteIdentity = UserDefaults.standard.string(forKey: remoteIdentityKey)?.nonEmpty
?? configRemote?["sshIdentity"] as? String
?? ""
self.remoteProjectRoot = UserDefaults.standard.string(forKey: remoteProjectRootKey)?.nonEmpty ?? ""
self.remoteCliPath = UserDefaults.standard.string(forKey: remoteCliPathKey)?.nonEmpty ?? ""
self.canvasEnabled = UserDefaults.standard.object(forKey: canvasEnabledKey) as? Bool ?? true
let execDefaults = ExecApprovalsStore.resolveDefaults()
self.execApprovalMode = ExecApprovalQuickMode.from(security: execDefaults.security, ask: execDefaults.ask)
@@ -517,7 +534,10 @@ final class AppState {
}
case .ssh:
changed = Self.updateGatewayString(&remote, key: "transport", value: nil) || changed
changed = Self.updateGatewayString(
&remote,
key: "transport",
value: RemoteTransport.ssh.rawValue) || changed
let sanitizedTarget = Self.sanitizeSSHTarget(draft.remoteTarget)
let expectedRemoteHost = CommandResolver.parseSSHTarget(sanitizedTarget)?.host ?? draft.remoteHost
@@ -561,7 +581,8 @@ final class AppState {
let hasRemoteUrl = !(remoteUrl?
.trimmingCharacters(in: .whitespacesAndNewlines)
.isEmpty ?? true)
let remoteTransport = GatewayRemoteConfig.resolveTransport(root: root)
let remoteResolution = GatewayRemoteConfig.resolveTransportResolution(root: root)
let remoteTransport = remoteResolution.transport
let desiredMode: ConnectionMode? = switch modeRaw {
case "local":
@@ -585,7 +606,7 @@ final class AppState {
if remoteTransport != self.remoteTransport {
self.remoteTransport = remoteTransport
}
let remoteUrlText = remoteUrl ?? ""
let remoteUrlText = remoteResolution.directURL?.absoluteString ?? remoteUrl ?? ""
if remoteUrlText != self.remoteUrl {
self.remoteUrl = remoteUrlText
}

View File

@@ -26,7 +26,7 @@ final class CLIInstallPrompter {
case .alertFirstButtonReturn:
Task { await self.installCLI() }
case .alertThirdButtonReturn:
self.openSettings(tab: .general)
self.openSettings(tab: .connection)
default:
break
}

View File

@@ -1,9 +1,27 @@
import SwiftUI
enum ConfigSchemaFormMode {
case full
case channelQuick
}
struct ConfigSchemaForm: View {
@Bindable var store: ChannelsStore
let schema: ConfigSchemaNode
let path: ConfigPath
let mode: ConfigSchemaFormMode
init(
store: ChannelsStore,
schema: ConfigSchemaNode,
path: ConfigPath,
mode: ConfigSchemaFormMode = .full)
{
self.store = store
self.schema = schema
self.path = path
self.mode = mode
}
var body: some View {
self.renderNode(self.schema, path: self.path)
@@ -12,7 +30,7 @@ struct ConfigSchemaForm: View {
private func renderNode(_ schema: ConfigSchemaNode, path: ConfigPath) -> AnyView {
let storedValue = self.store.configValue(at: path)
let value = storedValue ?? schema.explicitDefault
let label = hintForPath(path, hints: store.configUiHints)?.label ?? schema.title
let label = self.fieldLabel(for: schema, path: path)
let help = hintForPath(path, hints: store.configUiHints)?.help ?? schema.description
let variants = schema.anyOf.isEmpty ? schema.oneOf : schema.anyOf
@@ -62,18 +80,16 @@ struct ConfigSchemaForm: View {
.foregroundStyle(.secondary)
}
let properties = schema.properties
let sortedKeys = properties.keys.sorted { lhs, rhs in
let orderA = hintForPath(path + [.key(lhs)], hints: store.configUiHints)?.order ?? 0
let orderB = hintForPath(path + [.key(rhs)], hints: store.configUiHints)?.order ?? 0
if orderA != orderB { return orderA < orderB }
return lhs < rhs
}
let sortedKeys = self.visibleObjectKeys(properties: properties, path: path)
ForEach(sortedKeys, id: \ .self) { key in
if let child = properties[key] {
self.renderNode(child, path: path + [.key(key)])
}
}
if schema.allowsAdditionalProperties {
if sortedKeys.isEmpty, self.mode == .channelQuick, self.isChannelRoot(path) {
self.renderChannelQuickEmptyState()
}
if self.shouldRenderAdditionalProperties(schema, path: path, value: value) {
self.renderAdditionalProperties(schema, path: path, value: value)
}
})
@@ -100,6 +116,116 @@ struct ConfigSchemaForm: View {
}
}
private func fieldLabel(for schema: ConfigSchemaNode, path: ConfigPath) -> String? {
hintForPath(path, hints: self.store.configUiHints)?.label
?? schema.title
?? labelForConfigPath(path)
}
private func visibleObjectKeys(
properties: [String: ConfigSchemaNode],
path: ConfigPath) -> [String]
{
let sortedKeys = properties.keys.sorted { lhs, rhs in
let orderA = hintForPath(path + [.key(lhs)], hints: store.configUiHints)?.order ?? 0
let orderB = hintForPath(path + [.key(rhs)], hints: store.configUiHints)?.order ?? 0
if orderA != orderB { return orderA < orderB }
return lhs < rhs
}
guard self.mode == .channelQuick, self.isChannelRoot(path) else {
return sortedKeys
}
return sortedKeys.filter { key in
guard let child = properties[key] else { return false }
return self.shouldRenderChannelQuickField(key: key, schema: child, path: path + [.key(key)])
}
}
private func shouldRenderChannelQuickField(
key: String,
schema: ConfigSchemaNode,
path: ConfigPath) -> Bool
{
if hintForPath(path, hints: self.store.configUiHints)?.advanced == true {
return false
}
if Self.channelQuickKeys.contains(key) {
return self.isSimpleField(schema)
}
return self.store.configValue(at: path) != nil && self.isSimpleField(schema)
}
private func isSimpleField(_ schema: ConfigSchemaNode) -> Bool {
let variants = schema.anyOf.isEmpty ? schema.oneOf : schema.anyOf
let nonNullVariants = variants.filter { !$0.isNullSchema }
if !nonNullVariants.isEmpty {
return nonNullVariants.allSatisfy(self.isSimpleField)
}
if let enumValues = schema.enumValues {
return !enumValues.isEmpty
}
switch schema.schemaType {
case "boolean", "integer", "number", "string":
return true
default:
return false
}
}
private func shouldRenderAdditionalProperties(
_ schema: ConfigSchemaNode,
path: ConfigPath,
value: Any?) -> Bool
{
guard schema.allowsAdditionalProperties else { return false }
if self.mode != .channelQuick { return true }
guard let dict = value as? [String: Any] else { return false }
let reserved = Set(schema.properties.keys)
return dict.keys.contains { !reserved.contains($0) }
}
private func isChannelRoot(_ path: ConfigPath) -> Bool {
guard path.count == 2 else { return false }
guard case .key("channels") = path[0] else { return false }
guard case .key = path[1] else { return false }
return true
}
private func renderChannelQuickEmptyState() -> some View {
VStack(alignment: .leading, spacing: 4) {
Text("No quick settings for this channel.")
.font(.callout.weight(.semibold))
Text("Use Config for account, guild, action, and policy details.")
.font(.caption)
.foregroundStyle(.secondary)
}
}
private static let channelQuickKeys: Set<String> = [
"apiHash",
"apiId",
"appToken",
"baseUrl",
"botToken",
"configWrites",
"deviceName",
"dmPolicy",
"enabled",
"groupPolicy",
"historyLimit",
"mode",
"nativeCommands",
"nativeSkillCommands",
"phoneNumber",
"signingSecret",
"token",
"url",
"username",
"webhookUrl",
]
@ViewBuilder
private func renderStringField(
_ schema: ConfigSchemaNode,
@@ -353,7 +479,11 @@ struct ChannelConfigForm: View {
if self.store.configSchemaLoading {
ProgressView().controlSize(.small)
} else if let schema = store.channelConfigSchema(for: channelId) {
ConfigSchemaForm(store: self.store, schema: schema, path: [.key("channels"), .key(self.channelId)])
ConfigSchemaForm(
store: self.store,
schema: schema,
path: [.key("channels"), .key(self.channelId)],
mode: .channelQuick)
} else {
Text("Schema unavailable for this channel.")
.font(.caption)

View File

@@ -6,7 +6,7 @@ extension ChannelsSettings {
_ id: String,
as type: T.Type) -> T?
{
self.store.snapshot?.decodeChannel(id, as: type)
self.store.decodedChannel(id, as: type)
}
private func configuredChannelTint(configured: Bool, running: Bool, hasError: Bool, probeOk: Bool?) -> Color {
@@ -358,12 +358,16 @@ extension ChannelsSettings {
}
func ensureSelection() {
self.ensureSelection(in: self.orderedChannels)
}
func ensureSelection(in orderedChannels: [ChannelItem]) {
guard let selected = self.selectedChannel else {
self.selectedChannel = self.orderedChannels.first
self.selectedChannel = orderedChannels.first
return
}
if !self.orderedChannels.contains(selected) {
self.selectedChannel = self.orderedChannels.first
if !orderedChannels.contains(selected) {
self.selectedChannel = orderedChannels.first
}
}

View File

@@ -2,34 +2,49 @@ import SwiftUI
extension ChannelsSettings {
var body: some View {
HStack(spacing: 0) {
self.sidebar
let channels = self.orderedChannels
return HStack(spacing: 0) {
self.sidebar(channels: channels)
self.detail
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
.onAppear {
self.store.start()
self.ensureSelection()
self.updateActiveWork(active: self.isActive)
self.ensureSelection(in: channels)
}
.onChange(of: self.orderedChannels) { _, _ in
self.ensureSelection()
.onChange(of: self.isActive) { _, active in
self.updateActiveWork(active: active)
}
.onChange(of: channels) { _, newValue in
self.ensureSelection(in: newValue)
}
.onDisappear { self.store.stop() }
}
private var sidebar: some View {
SettingsSidebarScroll {
private func updateActiveWork(active: Bool) {
if active {
self.store.start()
} else {
self.store.stop()
}
}
private func sidebar(channels: [ChannelItem]) -> some View {
let enabled = channels.filter { self.channelEnabled($0) }
let available = channels.filter { !self.channelEnabled($0) }
return SettingsSidebarScroll {
LazyVStack(alignment: .leading, spacing: 8) {
if !self.enabledChannels.isEmpty {
if !enabled.isEmpty {
self.sidebarSectionHeader("Configured")
ForEach(self.enabledChannels) { channel in
ForEach(enabled) { channel in
self.sidebarRow(channel)
}
}
if !self.availableChannels.isEmpty {
if !available.isEmpty {
self.sidebarSectionHeader("Available")
ForEach(self.availableChannels) { channel in
ForEach(available) { channel in
self.sidebarRow(channel)
}
}

View File

@@ -11,9 +11,11 @@ struct ChannelsSettings: View {
}
@Bindable var store: ChannelsStore
let isActive: Bool
@State var selectedChannel: ChannelItem?
init(store: ChannelsStore = .shared) {
init(store: ChannelsStore = .shared, isActive: Bool = true) {
self.store = store
self.isActive = isActive
}
}

View File

@@ -2,42 +2,171 @@ import Foundation
import OpenClawProtocol
extension ChannelsStore {
func loadConfigSchema() async {
guard !self.configSchemaLoading else { return }
func loadConfigSchema(force: Bool = false) async {
let sourceKey = self.currentConfigCacheSourceKey()
self.resetConfigSchemaCacheIfSourceChanged(sourceKey)
if !force, self.configSchema != nil {
return
}
guard !self.queueConfigSchemaReloadIfLoading(sourceKey: sourceKey, force: force) else { return }
self.configSchemaLoading = true
defer { self.configSchemaLoading = false }
self.configSchemaLoadingSourceKey = sourceKey
defer {
self.configSchemaLoading = false
self.configSchemaLoadingSourceKey = nil
}
do {
let res: ConfigSchemaResponse = try await GatewayConnection.shared.requestDecoded(
method: .configSchema,
params: nil,
timeoutMs: 8000)
let schemaValue = res.schema.foundationValue
self.configSchema = ConfigSchemaNode(raw: schemaValue)
let hintValues = res.uihints.mapValues { $0.foundationValue }
self.configUiHints = decodeUiHints(hintValues)
} catch {
self.configStatus = error.localizedDescription
var requestSourceKey = sourceKey
while true {
self.configSchemaLoadingSourceKey = requestSourceKey
do {
let res: ConfigSchemaResponse = try await GatewayConnection.shared.requestDecoded(
method: .configSchema,
params: nil,
timeoutMs: 8000)
self.applyConfigSchemaResponse(res, sourceKey: requestSourceKey)
} catch {
self.configStatus = error.localizedDescription
}
guard self.configSchemaReloadPending else { break }
self.configSchemaReloadPending = false
requestSourceKey = self.currentConfigCacheSourceKey()
self.resetConfigSchemaCacheIfSourceChanged(requestSourceKey)
}
}
func loadConfig() async {
do {
let snap: ConfigSnapshot = try await GatewayConnection.shared.requestDecoded(
method: .configGet,
params: nil,
timeoutMs: 10000)
self.configStatus = snap.valid == false
? "Config invalid; fix it in ~/.openclaw/openclaw.json."
: nil
self.configRoot = snap.config?.mapValues { $0.foundationValue } ?? [:]
self.configDraft = cloneConfigValue(self.configRoot) as? [String: Any] ?? self.configRoot
self.configDirty = false
self.configLoaded = true
@discardableResult
func loadConfigSchemaLookup(path: String, force: Bool = false) async -> ConfigSchemaLookupNode? {
let sourceKey = self.currentConfigCacheSourceKey()
self.resetConfigSchemaCacheIfSourceChanged(sourceKey)
let normalizedPath = Self.normalizeConfigLookupPath(path)
if !force, let cached = self.configLookupNode(path: normalizedPath) {
return cached
}
if self.configLookupLoadingPaths.contains(normalizedPath) {
return self.configLookupNode(path: normalizedPath)
}
self.applyUIConfig(snap)
self.configLookupLoadingPaths.insert(normalizedPath)
defer { self.configLookupLoadingPaths.remove(normalizedPath) }
do {
let res: ConfigSchemaLookupResult = try await GatewayConnection.shared.requestDecoded(
method: .configSchemaLookup,
params: ["path": AnyCodable(normalizedPath)],
timeoutMs: 5000)
guard let node = self.makeConfigLookupNode(res) else {
self.configStatus = "Config schema lookup returned an unsupported payload."
return nil
}
self.applyConfigLookupNode(node, sourceKey: sourceKey)
return node
} catch {
self.configStatus = error.localizedDescription
return nil
}
}
func loadConfig(force: Bool = true) async {
let sourceKey = self.currentConfigCacheSourceKey()
self.resetConfigCacheIfSourceChanged(sourceKey)
if !force, self.configLoaded {
return
}
guard !self.queueConfigReloadIfLoading(sourceKey: sourceKey, force: force) else { return }
self.configLoading = true
self.configLoadingSourceKey = sourceKey
defer {
self.configLoading = false
self.configLoadingSourceKey = nil
}
var requestForce = force
var requestSourceKey = sourceKey
while true {
self.configLoadingSourceKey = requestSourceKey
do {
let snap: ConfigSnapshot = try await GatewayConnection.shared.requestDecoded(
method: .configGet,
params: nil,
timeoutMs: 10000)
self.applyConfigSnapshot(snap, sourceKey: requestSourceKey, force: requestForce)
} catch {
self.configStatus = error.localizedDescription
}
guard self.configForceReloadPending else { break }
self.configForceReloadPending = false
requestForce = true
requestSourceKey = self.currentConfigCacheSourceKey()
self.resetConfigCacheIfSourceChanged(requestSourceKey)
}
}
func applyConfigSnapshot(_ snap: ConfigSnapshot, sourceKey: String, force: Bool) {
guard self.configSourceKey == sourceKey else { return }
guard force || !self.configDirty else { return }
self.configStatus = snap.valid == false
? "Config invalid; fix it in ~/.openclaw/openclaw.json."
: nil
self.configRoot = snap.config?.mapValues { $0.foundationValue } ?? [:]
self.configDraft = cloneConfigValue(self.configRoot) as? [String: Any] ?? self.configRoot
self.configDirty = false
self.configLoaded = true
self.configSourceKey = sourceKey
self.applyUIConfig(snap)
}
func applyConfigSchemaResponse(_ res: ConfigSchemaResponse, sourceKey: String) {
guard self.configSchemaSourceKey == sourceKey else { return }
let schemaValue = res.schema.foundationValue
self.configSchema = ConfigSchemaNode(raw: schemaValue)
let hintValues = res.uihints.mapValues { $0.foundationValue }
self.configUiHints = decodeUiHints(hintValues)
self.configSchemaSourceKey = sourceKey
}
func configLookupNode(path: String) -> ConfigSchemaLookupNode? {
let normalizedPath = Self.normalizeConfigLookupPath(path)
if normalizedPath == "." {
return self.configLookupRoot
}
return self.configLookupCache[normalizedPath]
}
func makeConfigLookupNode(_ res: ConfigSchemaLookupResult) -> ConfigSchemaLookupNode? {
let schemaValue = res.schema.foundationValue
guard let schema = ConfigSchemaNode(raw: schemaValue) else { return nil }
let hint = res.hint.map { ConfigUiHint(raw: $0.mapValues(\.foundationValue)) }
let children = res.children.compactMap(ConfigSchemaLookupChild.init(raw:))
return ConfigSchemaLookupNode(
path: Self.normalizeConfigLookupPath(res.path),
schema: schema,
hint: hint,
hintPath: res.hintpath,
children: children)
}
func applyConfigLookupNode(_ node: ConfigSchemaLookupNode, sourceKey: String) {
guard self.configSchemaSourceKey == sourceKey else { return }
if node.path == "." {
self.configLookupRoot = node
} else {
self.configLookupCache[node.path] = node
}
if let hint = node.hint {
self.configUiHints[node.path] = hint
}
for child in node.children {
if let hint = child.hint {
self.configUiHints[child.path] = hint
}
}
}
@@ -85,7 +214,83 @@ extension ChannelsStore {
}
func reloadConfigDraft() async {
await self.loadConfig()
await self.loadConfig(force: true)
}
func resetConfigSchemaCacheIfSourceChanged(_ sourceKey: String) {
guard let cachedSourceKey = self.configSchemaSourceKey else {
self.configSchemaSourceKey = sourceKey
return
}
guard cachedSourceKey != sourceKey else { return }
self.configSchema = nil
self.configLookupRoot = nil
self.configLookupCache.removeAll(keepingCapacity: true)
self.configLookupLoadingPaths.removeAll(keepingCapacity: true)
self.configUiHints = [:]
self.configSchemaSourceKey = sourceKey
}
func resetConfigCacheIfSourceChanged(_ sourceKey: String) {
guard let cachedSourceKey = self.configSourceKey else {
self.configSourceKey = sourceKey
return
}
guard cachedSourceKey != sourceKey else { return }
self.configRoot = [:]
self.configDraft = [:]
self.configDirty = false
self.configLoaded = false
self.configSourceKey = sourceKey
}
func queueConfigReloadIfLoading(sourceKey: String, force: Bool) -> Bool {
guard self.configLoading else { return false }
if force || self.configLoadingSourceKey != sourceKey {
self.configForceReloadPending = true
}
return true
}
func queueConfigSchemaReloadIfLoading(sourceKey: String, force: Bool) -> Bool {
guard self.configSchemaLoading else { return false }
if force || self.configSchemaLoadingSourceKey != sourceKey {
self.configSchemaReloadPending = true
}
return true
}
private func currentConfigCacheSourceKey() -> String {
let root = OpenClawConfigFile.loadDict()
let settings = CommandResolver.connectionSettings(configRoot: root)
let env = ProcessInfo.processInfo.environment
return [
"mode:\(settings.mode.rawValue)",
"target:\(settings.target)",
"identity:\(settings.identity)",
"project:\(settings.projectRoot)",
"cli:\(settings.cliPath)",
"port:\(GatewayEnvironment.gatewayPort())",
"gateway:\(Self.configFingerprint(root["gateway"]))",
"token:\(Self.configFingerprint(env["OPENCLAW_GATEWAY_TOKEN"]))",
"password:\(Self.configFingerprint(env["OPENCLAW_GATEWAY_PASSWORD"]))",
].joined(separator: "|")
}
static func normalizeConfigLookupPath(_ path: String) -> String {
let trimmed = path.trimmingCharacters(in: .whitespacesAndNewlines)
return trimmed.isEmpty ? "." : trimmed
}
private static func configFingerprint(_ value: Any?) -> String {
guard let value else { return "nil" }
if JSONSerialization.isValidJSONObject(value),
let data = try? JSONSerialization.data(withJSONObject: value, options: [.sortedKeys])
{
return "\(data.count):\(data.hashValue)"
}
let text = String(describing: value)
return "\(text.count):\(text.hashValue)"
}
}

View File

@@ -22,12 +22,15 @@ func whatsappLoginWaitRequestTimeoutMs(
extension ChannelsStore {
func start() {
guard !self.isPreview else { return }
self.startCount += 1
guard self.startCount == 1 else { return }
guard self.pollTask == nil else { return }
self.pollTask = Task.detached { [weak self] in
guard let self else { return }
await self.refresh(probe: true)
await self.loadConfigSchema()
await self.loadConfig()
await self.refresh(probe: false)
async let schemaLoad: Void = self.loadConfigSchema()
async let configLoad: Void = self.loadConfig(force: false)
_ = await (schemaLoad, configLoad)
while !Task.isCancelled {
try? await Task.sleep(nanoseconds: UInt64(self.interval * 1_000_000_000))
await self.refresh(probe: false)
@@ -36,6 +39,10 @@ extension ChannelsStore {
}
func stop() {
guard !self.isPreview else { return }
guard self.startCount > 0 else { return }
self.startCount -= 1
guard self.startCount == 0 else { return }
self.pollTask?.cancel()
self.pollTask = nil
}
@@ -46,14 +53,15 @@ extension ChannelsStore {
defer { self.isRefreshing = false }
do {
let statusTimeoutMs = probe ? 8000 : 2500
let params: [String: AnyCodable] = [
"probe": AnyCodable(probe),
"timeoutMs": AnyCodable(8000),
"timeoutMs": AnyCodable(statusTimeoutMs),
]
let snap: ChannelsStatusSnapshot = try await GatewayConnection.shared.requestDecoded(
method: .channelsStatus,
params: params,
timeoutMs: 12000)
timeoutMs: probe ? 12000 : 5000)
self.snapshot = snap
self.lastSuccess = Date()
self.lastError = nil

View File

@@ -219,12 +219,64 @@ struct ConfigSnapshot: Codable {
let issues: [Issue]?
}
struct ConfigSchemaLookupChild: Identifiable {
let key: String
let path: String
let typeLabel: String?
let required: Bool
let hasChildren: Bool
let hint: ConfigUiHint?
let hintPath: String?
var id: String {
self.path
}
init?(raw: [String: AnyCodable]) {
guard let key = raw["key"]?.stringValue,
let path = raw["path"]?.stringValue
else {
return nil
}
self.key = key
self.path = path
if let type = raw["type"]?.stringValue {
self.typeLabel = type
} else if let types = raw["type"]?.arrayValue {
self.typeLabel = types.compactMap(\.stringValue).joined(separator: " / ")
} else {
self.typeLabel = nil
}
self.required = raw["required"]?.boolValue ?? false
self.hasChildren = raw["hasChildren"]?.boolValue ?? false
if let hint = raw["hint"]?.dictionaryValue {
self.hint = ConfigUiHint(raw: hint.mapValues(\.foundationValue))
} else {
self.hint = nil
}
self.hintPath = raw["hintPath"]?.stringValue
}
}
struct ConfigSchemaLookupNode {
let path: String
let schema: ConfigSchemaNode
let hint: ConfigUiHint?
let hintPath: String?
let children: [ConfigSchemaLookupChild]
}
@MainActor
@Observable
final class ChannelsStore {
static let shared = ChannelsStore()
var snapshot: ChannelsStatusSnapshot?
var snapshot: ChannelsStatusSnapshot? {
didSet {
self.decodedChannelCache.removeAll(keepingCapacity: true)
}
}
var lastError: String?
var lastSuccess: Date?
var isRefreshing = false
@@ -239,15 +291,27 @@ final class ChannelsStore {
var isSavingConfig = false
var configSchemaLoading = false
var configSchema: ConfigSchemaNode?
var configLookupRoot: ConfigSchemaLookupNode?
var configLookupCache: [String: ConfigSchemaLookupNode] = [:]
var configLookupLoadingPaths: Set<String> = []
var configUiHints: [String: ConfigUiHint] = [:]
var configSchemaSourceKey: String?
var configSchemaLoadingSourceKey: String?
var configSchemaReloadPending = false
var configLoading = false
var configLoadingSourceKey: String?
var configForceReloadPending = false
var configDraft: [String: Any] = [:]
var configDirty = false
let interval: TimeInterval = 45
let isPreview: Bool
var startCount = 0
var pollTask: Task<Void, Never>?
var configRoot: [String: Any] = [:]
var configLoaded = false
var configSourceKey: String?
@ObservationIgnored private var decodedChannelCache: [String: Any] = [:]
func channelMetaEntry(_ id: String) -> ChannelsStatusSnapshot.ChannelUiMetaEntry? {
self.snapshot?.channelMeta?.first(where: { $0.id == id })
@@ -290,6 +354,18 @@ final class ChannelsStore {
return self.snapshot?.channelOrder ?? []
}
func decodedChannel<T: Decodable>(_ id: String, as type: T.Type) -> T? {
let key = "\(id)#\(ObjectIdentifier(type))"
if let cached = self.decodedChannelCache[key] as? T {
return cached
}
guard let decoded = self.snapshot?.decodeChannel(id, as: type) else {
return nil
}
self.decodedChannelCache[key] = decoded
return decoded
}
func applyWhatsAppLoginWaitResult(_ result: WhatsAppLoginWaitResult) {
self.whatsappLoginMessage = result.message
self.whatsappLoginConnected = result.connected

View File

@@ -426,10 +426,15 @@ enum CommandResolver {
{
let root = configRoot ?? OpenClawConfigFile.loadDict()
let mode = ConnectionModeResolver.resolve(root: root, defaults: defaults).mode
let target = defaults.string(forKey: remoteTargetKey) ?? ""
let identity = defaults.string(forKey: remoteIdentityKey) ?? ""
let projectRoot = defaults.string(forKey: remoteProjectRootKey) ?? ""
let cliPath = defaults.string(forKey: remoteCliPathKey) ?? ""
let remote = (root["gateway"] as? [String: Any])?["remote"] as? [String: Any]
let target = defaults.string(forKey: remoteTargetKey)?.nonEmpty
?? remote?["sshTarget"] as? String
?? ""
let identity = defaults.string(forKey: remoteIdentityKey)?.nonEmpty
?? remote?["sshIdentity"] as? String
?? ""
let projectRoot = defaults.string(forKey: remoteProjectRootKey)?.nonEmpty ?? ""
let cliPath = defaults.string(forKey: remoteCliPathKey)?.nonEmpty ?? ""
return RemoteSettings(
mode: mode,
target: self.sanitizedTarget(target),

View File

@@ -208,6 +208,25 @@ func isSensitivePath(_ path: ConfigPath) -> Bool {
|| key.hasSuffix("key")
}
func labelForConfigPath(_ path: ConfigPath) -> String? {
for segment in path.reversed() {
if case let .key(key) = segment {
return humanizeConfigKey(key)
}
}
return nil
}
func humanizeConfigKey(_ key: String) -> String {
key.replacingOccurrences(of: "_", with: " ")
.replacingOccurrences(of: "-", with: " ")
.replacingOccurrences(
of: "([a-z0-9])([A-Z])",
with: "$1 $2",
options: .regularExpression)
.capitalized
}
func pathKey(_ path: ConfigPath) -> String {
path.compactMap { segment -> String? in
switch segment {

View File

@@ -6,8 +6,8 @@ struct ConfigSettings: View {
private let isNixMode = ProcessInfo.processInfo.isNixMode
@Bindable var store: ChannelsStore
@State private var hasLoaded = false
@State private var activeSectionKey: String?
@State private var activeSubsection: SubsectionSelection?
@State private var activePath: String?
@State private var failedLookupPaths: Set<String> = []
init(store: ChannelsStore = .shared) {
self.store = store
@@ -23,30 +23,32 @@ struct ConfigSettings: View {
guard !self.hasLoaded else { return }
guard !self.isPreview else { return }
self.hasLoaded = true
await self.store.loadConfigSchema()
await self.store.loadConfig()
Task { await self.store.loadConfig(force: false) }
_ = await self.store.loadConfigSchemaLookup(path: ".")
self.ensureSelection()
}
.task(id: self.activePath) {
guard let activePath = self.activePath else { return }
await self.loadPath(activePath)
}
.onAppear { self.ensureSelection() }
.onChange(of: self.store.configSchemaLoading) { _, loading in
if !loading { self.ensureSelection() }
.onChange(of: self.store.configLookupRoot?.path) { _, _ in
self.failedLookupPaths.removeAll()
self.ensureSelection()
}
}
}
extension ConfigSettings {
private enum SubsectionSelection: Hashable {
case all
case key(String)
}
private struct ConfigSection: Identifiable {
let key: String
let label: String
let help: String?
let node: ConfigSchemaNode
let path: String
let hasChildren: Bool
var id: String {
self.key
self.path
}
}
@@ -54,21 +56,22 @@ extension ConfigSettings {
let key: String
let label: String
let help: String?
let node: ConfigSchemaNode
let path: ConfigPath
let path: String
let hasChildren: Bool
var id: String {
self.key
self.path
}
}
private var sections: [ConfigSection] {
guard let schema = self.store.configSchema else { return [] }
return self.resolveSections(schema)
guard let root = self.store.configLookupRoot else { return [] }
return self.resolveSections(root.children)
}
private var activeSection: ConfigSection? {
self.sections.first { $0.key == self.activeSectionKey }
guard let activePath = self.activePath else { return nil }
return self.sections.first { activePath == $0.path || activePath.hasPrefix("\($0.path).") }
}
private var sidebar: some View {
@@ -91,16 +94,16 @@ extension ConfigSettings {
private var detail: some View {
VStack(alignment: .leading, spacing: 16) {
if self.store.configSchemaLoading {
if self.store.configLookupRoot == nil,
!self.hasLoaded || self.store.configLookupLoadingPaths.contains(".")
{
ProgressView().controlSize(.small)
} else if let section = self.activeSection {
self.sectionDetail(section)
} else if self.store.configSchema != nil {
} else if self.store.configLookupRoot != nil {
self.emptyDetail
} else {
Text("Schema unavailable.")
.font(.caption)
.foregroundStyle(.secondary)
self.schemaUnavailableDetail
}
}
.frame(minWidth: 460, maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
@@ -117,6 +120,18 @@ extension ConfigSettings {
.padding(.vertical, 18)
}
private var schemaUnavailableDetail: some View {
VStack(alignment: .leading, spacing: 8) {
self.header
Text(self.store.configStatus ?? "Schema unavailable.")
.font(.callout)
.foregroundStyle(.secondary)
self.actionRow
}
.padding(.horizontal, 24)
.padding(.vertical, 18)
}
private func sectionDetail(_ section: ConfigSection) -> some View {
ScrollView(.vertical) {
VStack(alignment: .leading, spacing: 16) {
@@ -176,13 +191,14 @@ extension ConfigSettings {
Button(self.store.isSavingConfig ? "Saving…" : "Save") {
Task { await self.store.saveConfigDraft() }
}
.disabled(self.isNixMode || self.store.isSavingConfig || !self.store.configDirty)
.disabled(self.isNixMode || self.store.isSavingConfig || !self.store.configLoaded || !self.store
.configDirty)
}
.buttonStyle(.bordered)
}
private func sidebarSection(_ section: ConfigSection) -> some View {
let isExpanded = self.activeSectionKey == section.key
let isExpanded = self.activePath == section.path || self.activePath?.hasPrefix("\(section.path).") == true
let subsections = isExpanded ? self.resolveSubsections(for: section) : []
return VStack(alignment: .leading, spacing: 2) {
@@ -200,7 +216,7 @@ extension ConfigSettings {
.padding(.vertical, 5)
.padding(.horizontal, 8)
.frame(maxWidth: .infinity, alignment: .leading)
.background(isExpanded && subsections.isEmpty
.background(self.activePath == section.path
? Color.accentColor.opacity(0.18)
: Color.clear)
.clipShape(RoundedRectangle(cornerRadius: 8, style: .continuous))
@@ -211,9 +227,8 @@ extension ConfigSettings {
if isExpanded, !subsections.isEmpty {
VStack(alignment: .leading, spacing: 1) {
self.sidebarSubRow(title: "All", key: nil, sectionKey: section.key)
ForEach(subsections) { sub in
self.sidebarSubRow(title: sub.label, key: sub.key, sectionKey: section.key)
self.sidebarSubRow(sub)
}
}
.padding(.leading, 20)
@@ -223,21 +238,12 @@ extension ConfigSettings {
.animation(.easeInOut(duration: 0.18), value: isExpanded)
}
private func sidebarSubRow(title: String, key: String?, sectionKey: String) -> some View {
let isSelected: Bool = {
guard self.activeSectionKey == sectionKey else { return false }
if let key { return self.activeSubsection == .key(key) }
return self.activeSubsection == .all
}()
private func sidebarSubRow(_ subsection: ConfigSubsection) -> some View {
let isSelected = self.activePath == subsection.path
return Button {
if let key {
self.activeSubsection = .key(key)
} else {
self.activeSubsection = .all
}
self.selectPath(subsection.path)
} label: {
Text(title)
Text(subsection.label)
.font(.callout)
.lineLimit(1)
.padding(.vertical, 4)
@@ -252,123 +258,190 @@ extension ConfigSettings {
}
private func sectionForm(_ section: ConfigSection) -> some View {
let subsection = self.activeSubsection
let defaultPath: ConfigPath = [.key(section.key)]
let subsections = self.resolveSubsections(for: section)
let resolved: (ConfigSchemaNode, ConfigPath) = {
if case let .key(key) = subsection,
let match = subsections.first(where: { $0.key == key })
{
return (match.node, match.path)
let path = self.activePath ?? section.path
if self.store.configLookupLoadingPaths.contains(path) {
return AnyView(ProgressView().controlSize(.small))
}
guard let node = self.store.configLookupNode(path: path) else {
if self.failedLookupPaths.contains(path) {
return AnyView(self.lookupUnavailable(path: path))
}
return (self.resolvedSchemaNode(section.node), defaultPath)
}()
return ConfigSchemaForm(store: self.store, schema: resolved.0, path: resolved.1)
.disabled(self.isNixMode)
return AnyView(ProgressView().controlSize(.small))
}
if !node.children.isEmpty, !Self.shouldRenderFormEditor(for: node.schema) {
return AnyView(self.lookupChildrenList(node))
}
guard self.store.configLoaded else {
return AnyView(
HStack(spacing: 8) {
ProgressView().controlSize(.small)
Text("Loading current values…")
.font(.caption)
.foregroundStyle(.secondary)
})
}
guard let configPath = Self.configPath(from: node.path) else {
return AnyView(
Text("Wildcard config entries are edited from their concrete key.")
.font(.caption)
.foregroundStyle(.secondary))
}
return AnyView(
ConfigSchemaForm(store: self.store, schema: node.schema, path: configPath)
.disabled(self.isNixMode))
}
private func ensureSelection() {
guard let schema = self.store.configSchema else { return }
let sections = self.resolveSections(schema)
let sections = self.sections
guard !sections.isEmpty else { return }
let active = sections.first { $0.key == self.activeSectionKey } ?? sections[0]
if self.activeSectionKey != active.key {
self.activeSectionKey = active.key
}
self.ensureSubsection(for: active)
}
private func ensureSubsection(for section: ConfigSection) {
let subsections = self.resolveSubsections(for: section)
guard !subsections.isEmpty else {
self.activeSubsection = nil
if let activePath = self.activePath,
sections.contains(where: { activePath == $0.path || activePath.hasPrefix("\($0.path).") })
{
return
}
switch self.activeSubsection {
case .all:
return
case let .key(key):
if subsections.contains(where: { $0.key == key }) { return }
case .none:
break
}
if let first = subsections.first {
self.activeSubsection = .key(first.key)
}
self.selectSection(sections[0])
}
private func selectSection(_ section: ConfigSection) {
guard self.activeSectionKey != section.key else { return }
self.activeSectionKey = section.key
let subsections = self.resolveSubsections(for: section)
if let first = subsections.first {
self.activeSubsection = .key(first.key)
} else {
self.activeSubsection = nil
self.activePath = section.path
}
private func selectPath(_ path: String) {
self.activePath = path
}
private func lookupUnavailable(path: String) -> some View {
VStack(alignment: .leading, spacing: 8) {
Text(self.store.configStatus ?? "Schema unavailable.")
.font(.callout)
.foregroundStyle(.secondary)
Button("Retry") {
self.failedLookupPaths.remove(path)
Task { await self.loadPath(path) }
}
.buttonStyle(.bordered)
}
}
private func resolveSections(_ root: ConfigSchemaNode) -> [ConfigSection] {
let node = self.resolvedSchemaNode(root)
let hints = self.store.configUiHints
let keys = node.properties.keys.sorted { lhs, rhs in
let orderA = hintForPath([.key(lhs)], hints: hints)?.order ?? 0
let orderB = hintForPath([.key(rhs)], hints: hints)?.order ?? 0
if orderA != orderB { return orderA < orderB }
return lhs < rhs
private func lookupChildrenList(_ node: ConfigSchemaLookupNode) -> some View {
VStack(alignment: .leading, spacing: 8) {
ForEach(node.children) { child in
Button {
self.selectPath(child.path)
} label: {
HStack(spacing: 8) {
VStack(alignment: .leading, spacing: 2) {
Text(self.label(for: child))
.font(.callout.weight(.semibold))
if let help = child.hint?.help {
Text(help)
.font(.caption)
.foregroundStyle(.secondary)
.lineLimit(2)
} else if let type = child.typeLabel {
Text(type)
.font(.caption)
.foregroundStyle(.secondary)
}
}
Spacer()
if child.required {
Text("Required")
.font(.caption)
.foregroundStyle(.secondary)
}
Image(systemName: child.hasChildren ? "chevron.right" : "slider.horizontal.3")
.font(.caption.weight(.semibold))
.foregroundStyle(.tertiary)
}
.padding(.vertical, 8)
.padding(.horizontal, 10)
.background(Color.primary.opacity(0.04))
.clipShape(RoundedRectangle(cornerRadius: 8, style: .continuous))
.contentShape(Rectangle())
}
.buttonStyle(.plain)
}
}
}
return keys.compactMap { key in
guard let child = node.properties[key] else { return nil }
let path: ConfigPath = [.key(key)]
let hint = hintForPath(path, hints: hints)
let label = hint?.label
?? child.title
?? self.humanize(key)
let help = hint?.help ?? child.description
return ConfigSection(key: key, label: label, help: help, node: child)
}
private func resolveSections(_ children: [ConfigSchemaLookupChild]) -> [ConfigSection] {
children
.sorted(by: self.sortLookupChildren)
.map { child in
ConfigSection(
key: child.key,
label: self.label(for: child),
help: child.hint?.help,
path: child.path,
hasChildren: child.hasChildren)
}
}
private func resolveSubsections(for section: ConfigSection) -> [ConfigSubsection] {
let node = self.resolvedSchemaNode(section.node)
guard node.schemaType == "object" else { return [] }
let hints = self.store.configUiHints
let keys = node.properties.keys.sorted { lhs, rhs in
let orderA = hintForPath([.key(section.key), .key(lhs)], hints: hints)?.order ?? 0
let orderB = hintForPath([.key(section.key), .key(rhs)], hints: hints)?.order ?? 0
if orderA != orderB { return orderA < orderB }
return lhs < rhs
guard let node = self.store.configLookupNode(path: section.path) else {
return []
}
return node.children
.sorted(by: self.sortLookupChildren)
.map { child in
ConfigSubsection(
key: child.key,
label: self.label(for: child),
help: child.hint?.help,
path: child.path,
hasChildren: child.hasChildren)
}
}
return keys.compactMap { key in
guard let child = node.properties[key] else { return nil }
let path: ConfigPath = [.key(section.key), .key(key)]
let hint = hintForPath(path, hints: hints)
let label = hint?.label
?? child.title
?? self.humanize(key)
let help = hint?.help ?? child.description
return ConfigSubsection(
key: key,
label: label,
help: help,
node: child,
path: path)
private func loadPath(_ path: String) async {
guard self.store.configLookupNode(path: path) == nil else {
self.failedLookupPaths.remove(path)
return
}
guard !self.store.configLookupLoadingPaths.contains(path) else { return }
if await self.store.loadConfigSchemaLookup(path: path) == nil {
self.failedLookupPaths.insert(path)
} else {
self.failedLookupPaths.remove(path)
}
}
private func resolvedSchemaNode(_ node: ConfigSchemaNode) -> ConfigSchemaNode {
let variants = node.anyOf.isEmpty ? node.oneOf : node.anyOf
if !variants.isEmpty {
let nonNull = variants.filter { !$0.isNullSchema }
if nonNull.count == 1, let only = nonNull.first { return only }
private func label(for child: ConfigSchemaLookupChild) -> String {
child.hint?.label
?? self.humanize(child.key)
}
private func sortLookupChildren(_ lhs: ConfigSchemaLookupChild, _ rhs: ConfigSchemaLookupChild) -> Bool {
let orderA = lhs.hint?.order ?? 0
let orderB = rhs.hint?.order ?? 0
if orderA != orderB { return orderA < orderB }
return lhs.key < rhs.key
}
private static func configPath(from lookupPath: String) -> ConfigPath? {
guard lookupPath != "." else { return [] }
let normalized = lookupPath
.replacingOccurrences(of: "[", with: ".")
.replacingOccurrences(of: "]", with: "")
let parts = normalized
.split(separator: ".")
.map(String.init)
.filter { !$0.isEmpty }
guard !parts.contains("*") else { return nil }
return parts.map { part in
if let index = Int(part) {
return .index(index)
}
return .key(part)
}
return node
}
private static func shouldRenderFormEditor(for schema: ConfigSchemaNode) -> Bool {
if schema.schemaType == "array" { return true }
return schema.additionalProperties != nil
}
private func humanize(_ key: String) -> String {

View File

@@ -9,31 +9,51 @@ struct ContextRootMenuLabelView: View {
MenuItemHighlightColors.palette(self.isHighlighted)
}
private var usesStackedLayout: Bool {
self.subtitle.count > 28 || self.subtitle.contains("\n")
}
var body: some View {
HStack(alignment: .firstTextBaseline, spacing: 8) {
Text("Context")
.font(.callout.weight(.semibold))
.foregroundStyle(self.palette.primary)
.lineLimit(1)
.layoutPriority(1)
HStack(alignment: self.usesStackedLayout ? .top : .firstTextBaseline, spacing: 8) {
VStack(alignment: .leading, spacing: 2) {
Text("Context")
.font(.callout.weight(.semibold))
.foregroundStyle(self.palette.primary)
.lineLimit(1)
if self.usesStackedLayout {
self.subtitleText
.lineLimit(5)
.fixedSize(horizontal: false, vertical: true)
}
}
.layoutPriority(1)
Spacer(minLength: 8)
Text(self.subtitle)
.font(.caption.monospacedDigit())
.foregroundStyle(self.palette.secondary)
.lineLimit(1)
.truncationMode(.tail)
.layoutPriority(2)
if !self.usesStackedLayout {
self.subtitleText
.lineLimit(1)
.layoutPriority(2)
}
Image(systemName: "chevron.right")
.font(.caption.weight(.semibold))
.foregroundStyle(self.palette.secondary)
.padding(.leading, 2)
.padding(.top, self.usesStackedLayout ? 2 : 0)
}
.padding(.vertical, 8)
.padding(.vertical, self.usesStackedLayout ? 7 : 8)
.padding(.leading, 22)
.padding(.trailing, 14)
.frame(width: max(1, self.width), alignment: .leading)
}
private var subtitleText: some View {
Text(self.subtitle)
.font(.caption.monospacedDigit())
.foregroundStyle(self.palette.secondary)
.multilineTextAlignment(.leading)
.truncationMode(.tail)
}
}

View File

@@ -240,6 +240,12 @@ final class ControlChannel {
case .timedOut:
return "Gateway request timed out; check gateway on localhost:\(port)."
case .notConnectedToInternet:
if Self.isLikelyLocalNetworkPermissionBlock() {
return """
macOS is blocking OpenClaw Local Network access.
Allow OpenClaw in System Settings → Privacy & Security → Local Network, then relaunch the app.
"""
}
return "No network connectivity; cannot reach gateway."
default:
break
@@ -257,6 +263,22 @@ final class ControlChannel {
return "Gateway error: \(trimmed)"
}
private static func isLikelyLocalNetworkPermissionBlock() -> Bool {
let root = OpenClawConfigFile.loadDict()
let resolution = GatewayRemoteConfig.resolveTransportResolution(root: root)
guard ConnectionModeResolver.resolve(root: root).mode == .remote,
resolution.transport == .direct,
let url = resolution.directURL,
url.scheme?.lowercased() == "ws",
let host = url.host,
GatewayRemoteConfig.isTrustedPlaintextRemoteHost(host),
!LoopbackHost.isLoopbackHost(host)
else {
return false
}
return true
}
private func scheduleRecovery(reason: String) {
let now = Date()
if let last = self.lastRecoveryAt, now.timeIntervalSince(last) < 10 { return }

View File

@@ -20,12 +20,11 @@ enum CritterIconRenderer {
private struct Geometry {
let bodyRect: CGRect
let bodyCorner: CGFloat
let leftArmRect: CGRect
let rightArmRect: CGRect
let leftEarRect: CGRect
let rightEarRect: CGRect
let earCorner: CGFloat
let earW: CGFloat
let earH: CGFloat
let antennaLineWidth: CGFloat
let legW: CGFloat
let legH: CGFloat
let legSpacing: CGFloat
@@ -33,7 +32,7 @@ enum CritterIconRenderer {
let legYBase: CGFloat
let legLift: CGFloat
let legHeightScale: CGFloat
let eyeW: CGFloat
let eyeSize: CGSize
let eyeY: CGFloat
let eyeOffset: CGFloat
@@ -43,46 +42,60 @@ enum CritterIconRenderer {
let snapX = canvas.snapX
let snapY = canvas.snapY
let bodyW = snapX(w * 0.78)
let bodyH = snapY(h * 0.58)
let bodyW = snapX(w * 0.68)
let bodyH = snapY(h * 0.68)
let bodyX = snapX((w - bodyW) / 2)
let bodyY = snapY(h * 0.36)
let bodyCorner = snapX(w * 0.09)
let bodyY = snapY(h * 0.24)
let earW = snapX(w * 0.22)
let earH = snapY(bodyH * 0.54 * earScale * (1 - 0.08 * abs(earWiggle)))
let earCorner = snapX(earW * 0.24)
let armSize = snapX(w * 0.2)
let armY = snapY(bodyY + bodyH * 0.36)
let leftArmRect = CGRect(
x: snapX(bodyX - armSize * 0.62),
y: armY,
width: armSize,
height: armSize)
let rightArmRect = CGRect(
x: snapX(bodyX + bodyW - armSize * 0.38),
y: armY,
width: armSize,
height: armSize)
let antennaW = snapX(w * 0.22)
let antennaH = snapY(min(bodyH * 0.24 * earScale, h * 0.19))
let antennaLineWidth = max(snapX(w * 0.095), canvas.stepX * 2) * min(1.2, 0.94 + earScale * 0.06)
let antennaLift = snapY(earWiggle * 0.35)
let leftEarRect = CGRect(
x: snapX(bodyX - earW * 0.55 + earWiggle),
y: snapY(bodyY + bodyH * 0.08 + earWiggle * 0.4),
width: earW,
height: earH)
x: snapX(bodyX + bodyW * 0.18 - antennaW * 0.35 - earWiggle * 0.28),
y: snapY(bodyY + bodyH * 0.86 + antennaLift),
width: antennaW,
height: antennaH)
let rightEarRect = CGRect(
x: snapX(bodyX + bodyW - earW * 0.45 - earWiggle),
y: snapY(bodyY + bodyH * 0.08 - earWiggle * 0.4),
width: earW,
height: earH)
x: snapX(bodyX + bodyW * 0.82 - antennaW * 0.65 + earWiggle * 0.28),
y: snapY(bodyY + bodyH * 0.86 - antennaLift),
width: antennaW,
height: antennaH)
let legW = snapX(w * 0.11)
let legH = snapY(h * 0.26)
let legSpacing = snapX(w * 0.085)
let legsWidth = snapX(4 * legW + 3 * legSpacing)
let legW = snapX(w * 0.15)
let legH = snapY(h * 0.25)
let legSpacing = snapX(w * 0.16)
let legsWidth = snapX(2 * legW + legSpacing)
let legStartX = snapX((w - legsWidth) / 2)
let legLift = snapY(legH * 0.35 * legWiggle)
let legYBase = snapY(bodyY - legH + h * 0.05)
let legYBase = snapY(bodyY - legH * 0.58)
let legHeightScale = 1 - 0.12 * legWiggle
let eyeW = snapX(bodyW * 0.2)
let eyeY = snapY(bodyY + bodyH * 0.56)
let eyeOffset = snapX(bodyW * 0.24)
let eyeSize = CGSize(
width: snapX(bodyW * 0.15),
height: snapY(bodyH * 0.2))
let eyeY = snapY(bodyY + bodyH * 0.58)
let eyeOffset = snapX(bodyW * 0.22)
self.bodyRect = CGRect(x: bodyX, y: bodyY, width: bodyW, height: bodyH)
self.bodyCorner = bodyCorner
self.leftArmRect = leftArmRect
self.rightArmRect = rightArmRect
self.leftEarRect = leftEarRect
self.rightEarRect = rightEarRect
self.earCorner = earCorner
self.earW = earW
self.earH = earH
self.antennaLineWidth = antennaLineWidth
self.legW = legW
self.legH = legH
self.legSpacing = legSpacing
@@ -90,7 +103,7 @@ enum CritterIconRenderer {
self.legYBase = legYBase
self.legLift = legLift
self.legHeightScale = legHeightScale
self.eyeW = eyeW
self.eyeSize = eyeSize
self.eyeY = eyeY
self.eyeOffset = eyeOffset
}
@@ -98,8 +111,6 @@ enum CritterIconRenderer {
private struct FaceOptions {
let blink: CGFloat
let earHoles: Bool
let earScale: CGFloat
let eyesClosedLines: Bool
}
@@ -125,16 +136,18 @@ enum CritterIconRenderer {
}
NSGraphicsContext.current = context
context.imageInterpolation = .none
context.cgContext.setShouldAntialias(false)
context.cgContext.setShouldAntialias(true)
let canvas = self.makeCanvas(for: rep, context: context)
let geometry = Geometry(canvas: canvas, legWiggle: legWiggle, earWiggle: earWiggle, earScale: earScale)
let geometry = Geometry(
canvas: canvas,
legWiggle: legWiggle,
earWiggle: earWiggle,
earScale: earHoles ? max(earScale, 1.2) : earScale)
self.drawBody(in: canvas, geometry: geometry)
let face = FaceOptions(
blink: blink,
earHoles: earHoles,
earScale: earScale,
eyesClosedLines: eyesClosedLines)
self.drawFace(in: canvas, geometry: geometry, options: face)
@@ -186,25 +199,41 @@ enum CritterIconRenderer {
}
private static func drawBody(in canvas: Canvas, geometry: Geometry) {
canvas.context.setStrokeColor(NSColor.labelColor.cgColor)
canvas.context.setLineWidth(geometry.antennaLineWidth)
canvas.context.setLineCap(.round)
canvas.context.setLineJoin(.round)
let leftStart = CGPoint(
x: canvas.snapX(geometry.bodyRect.minX + geometry.bodyRect.width * 0.34),
y: canvas.snapY(geometry.bodyRect.maxY - geometry.antennaLineWidth * 0.22))
let leftEnd = CGPoint(
x: canvas.snapX(geometry.leftEarRect.minX),
y: canvas.snapY(geometry.leftEarRect.maxY))
let leftControl = CGPoint(
x: canvas.snapX(geometry.leftEarRect.midX),
y: canvas.snapY(geometry.leftEarRect.minY))
let rightStart = CGPoint(
x: canvas.snapX(geometry.bodyRect.maxX - geometry.bodyRect.width * 0.34),
y: canvas.snapY(geometry.bodyRect.maxY - geometry.antennaLineWidth * 0.22))
let rightEnd = CGPoint(
x: canvas.snapX(geometry.rightEarRect.maxX),
y: canvas.snapY(geometry.rightEarRect.maxY))
let rightControl = CGPoint(
x: canvas.snapX(geometry.rightEarRect.midX),
y: canvas.snapY(geometry.rightEarRect.minY))
let antennae = CGMutablePath()
antennae.move(to: leftStart)
antennae.addQuadCurve(to: leftEnd, control: leftControl)
antennae.move(to: rightStart)
antennae.addQuadCurve(to: rightEnd, control: rightControl)
canvas.context.addPath(antennae)
canvas.context.strokePath()
canvas.context.setFillColor(NSColor.labelColor.cgColor)
canvas.context.addPath(CGPath(
roundedRect: geometry.bodyRect,
cornerWidth: geometry.bodyCorner,
cornerHeight: geometry.bodyCorner,
transform: nil))
canvas.context.addPath(CGPath(
roundedRect: geometry.leftEarRect,
cornerWidth: geometry.earCorner,
cornerHeight: geometry.earCorner,
transform: nil))
canvas.context.addPath(CGPath(
roundedRect: geometry.rightEarRect,
cornerWidth: geometry.earCorner,
cornerHeight: geometry.earCorner,
transform: nil))
for i in 0..<4 {
for i in 0..<2 {
let x = geometry.legStartX + CGFloat(i) * (geometry.legW + geometry.legSpacing)
let lift = i % 2 == 0 ? geometry.legLift : -geometry.legLift
let rect = CGRect(
@@ -218,6 +247,10 @@ enum CritterIconRenderer {
cornerHeight: geometry.legW * 0.34,
transform: nil))
}
canvas.context.addEllipse(in: geometry.leftArmRect)
canvas.context.addEllipse(in: geometry.rightArmRect)
canvas.context.addEllipse(in: geometry.bodyRect)
canvas.context.fillPath()
}
@@ -236,35 +269,8 @@ enum CritterIconRenderer {
x: canvas.snapX(canvas.w / 2 + geometry.eyeOffset),
y: canvas.snapY(geometry.eyeY))
if options.earHoles || options.earScale > 1.05 {
let holeW = canvas.snapX(geometry.earW * 0.6)
let holeH = canvas.snapY(geometry.earH * 0.46)
let holeCorner = canvas.snapX(holeW * 0.34)
let leftHoleRect = CGRect(
x: canvas.snapX(geometry.leftEarRect.midX - holeW / 2),
y: canvas.snapY(geometry.leftEarRect.midY - holeH / 2 + geometry.earH * 0.04),
width: holeW,
height: holeH)
let rightHoleRect = CGRect(
x: canvas.snapX(geometry.rightEarRect.midX - holeW / 2),
y: canvas.snapY(geometry.rightEarRect.midY - holeH / 2 + geometry.earH * 0.04),
width: holeW,
height: holeH)
canvas.context.addPath(CGPath(
roundedRect: leftHoleRect,
cornerWidth: holeCorner,
cornerHeight: holeCorner,
transform: nil))
canvas.context.addPath(CGPath(
roundedRect: rightHoleRect,
cornerWidth: holeCorner,
cornerHeight: holeCorner,
transform: nil))
}
if options.eyesClosedLines {
let lineW = canvas.snapX(geometry.eyeW * 0.95)
let lineW = canvas.snapX(geometry.eyeSize.width * 1.15)
let lineH = canvas.snapY(max(canvas.stepY * 2, geometry.bodyRect.height * 0.06))
let corner = canvas.snapX(lineH * 0.6)
let leftRect = CGRect(
@@ -289,34 +295,20 @@ enum CritterIconRenderer {
transform: nil))
} else {
let eyeOpen = max(0.05, 1 - options.blink)
let eyeH = canvas.snapY(geometry.bodyRect.height * 0.26 * eyeOpen)
let eyeH = canvas.snapY(geometry.eyeSize.height * eyeOpen)
let leftRect = CGRect(
x: canvas.snapX(leftCenter.x - geometry.eyeSize.width / 2),
y: canvas.snapY(leftCenter.y - eyeH / 2),
width: geometry.eyeSize.width,
height: eyeH)
let rightRect = CGRect(
x: canvas.snapX(rightCenter.x - geometry.eyeSize.width / 2),
y: canvas.snapY(rightCenter.y - eyeH / 2),
width: geometry.eyeSize.width,
height: eyeH)
let left = CGMutablePath()
left.move(to: CGPoint(
x: canvas.snapX(leftCenter.x - geometry.eyeW / 2),
y: canvas.snapY(leftCenter.y - eyeH)))
left.addLine(to: CGPoint(
x: canvas.snapX(leftCenter.x + geometry.eyeW / 2),
y: canvas.snapY(leftCenter.y)))
left.addLine(to: CGPoint(
x: canvas.snapX(leftCenter.x - geometry.eyeW / 2),
y: canvas.snapY(leftCenter.y + eyeH)))
left.closeSubpath()
let right = CGMutablePath()
right.move(to: CGPoint(
x: canvas.snapX(rightCenter.x + geometry.eyeW / 2),
y: canvas.snapY(rightCenter.y - eyeH)))
right.addLine(to: CGPoint(
x: canvas.snapX(rightCenter.x - geometry.eyeW / 2),
y: canvas.snapY(rightCenter.y)))
right.addLine(to: CGPoint(
x: canvas.snapX(rightCenter.x + geometry.eyeW / 2),
y: canvas.snapY(rightCenter.y + eyeH)))
right.closeSubpath()
canvas.context.addPath(left)
canvas.context.addPath(right)
canvas.context.addEllipse(in: leftRect)
canvas.context.addEllipse(in: rightRect)
}
canvas.context.fillPath()

View File

@@ -2,15 +2,20 @@ import SwiftUI
extension CronSettings {
var body: some View {
VStack(alignment: .leading, spacing: 12) {
VStack(alignment: .leading, spacing: 16) {
self.header
self.schedulerBanner
self.content
Spacer(minLength: 0)
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
.padding(.leading, 18)
.padding(.trailing, SettingsLayout.scrollbarGutter)
.onAppear {
self.store.start()
self.channelsStore.start()
self.updateActiveWork(active: self.isActive)
}
.onChange(of: self.isActive) { _, active in
self.updateActiveWork(active: active)
}
.onDisappear {
self.store.stop()
@@ -48,10 +53,16 @@ extension CronSettings {
Text(job.displayName)
}
}
.onChange(of: self.store.selectedJobId) { _, newValue in
guard let newValue else { return }
Task { await self.store.refreshRuns(jobId: newValue) }
}
}
private func updateActiveWork(active: Bool) {
if active {
self.store.start()
self.channelsStore.start()
} else {
self.store.stop()
self.channelsStore.stop()
}
}
var schedulerBanner: some View {
@@ -89,16 +100,18 @@ extension CronSettings {
}
var header: some View {
HStack(alignment: .top) {
VStack(alignment: .leading, spacing: 4) {
Text("Cron")
.font(.headline)
Text("Manage Gateway cron jobs (main session vs isolated runs) and inspect run history.")
.font(.footnote)
HStack(alignment: .top, spacing: 16) {
VStack(alignment: .leading, spacing: 5) {
Text("Cron Jobs")
.font(.title3.weight(.semibold))
Text("Manage Gateway cron jobs and inspect run history.")
.font(.callout)
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
}
Spacer()
Spacer(minLength: 16)
HStack(spacing: 8) {
Button {
Task { await self.store.refreshJobs() }
@@ -133,14 +146,34 @@ extension CronSettings {
.foregroundStyle(.secondary)
}
List(selection: self.$store.selectedJobId) {
ForEach(self.store.jobs) { job in
self.jobRow(job)
.tag(job.id)
ScrollView(.vertical) {
LazyVStack(alignment: .leading, spacing: 4) {
ForEach(self.store.jobs) { job in
Button {
self.selectJob(job.id)
} label: {
self.jobRow(job)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, 8)
.background(
self.store.selectedJobId == job.id
? Color.accentColor.opacity(0.18) : .clear)
.clipShape(RoundedRectangle(cornerRadius: 8, style: .continuous))
}
.buttonStyle(.plain)
.contextMenu { self.jobContextMenu(job) }
}
if self.store.jobs.isEmpty {
Text("No cron jobs yet.")
.font(.footnote)
.foregroundStyle(.secondary)
.padding(.horizontal, 8)
.padding(.vertical, 6)
}
}
.padding(.vertical, 4)
}
.listStyle(.inset)
}
.frame(width: 250)
@@ -151,6 +184,11 @@ extension CronSettings {
}
}
private func selectJob(_ id: String) {
self.store.selectedJobId = id
Task { await self.store.refreshRuns(jobId: id) }
}
@ViewBuilder
var detail: some View {
if let selected = self.selectedJob {

View File

@@ -104,7 +104,6 @@ extension CronSettings {
store.runEntries = [run]
let view = CronSettings(store: store, channelsStore: ChannelsStore(isPreview: true))
_ = view.body
_ = view.jobRow(job)
_ = view.jobContextMenu(job)
_ = view.detailHeader(job)

View File

@@ -4,14 +4,16 @@ import SwiftUI
struct CronSettings: View {
@Bindable var store: CronJobsStore
@Bindable var channelsStore: ChannelsStore
let isActive: Bool
@State var showEditor = false
@State var editingJob: CronJob?
@State var editorError: String?
@State var isSaving = false
@State var confirmDelete: CronJob?
init(store: CronJobsStore = .shared, channelsStore: ChannelsStore = .shared) {
init(store: CronJobsStore = .shared, channelsStore: ChannelsStore = .shared, isActive: Bool = true) {
self.store = store
self.channelsStore = channelsStore
self.isActive = isActive
}
}

View File

@@ -0,0 +1,135 @@
import AppKit
import Foundation
import OpenClawKit
import OSLog
private let dashboardManagerLogger = Logger(subsystem: "ai.openclaw", category: "DashboardManager")
@MainActor
final class DashboardManager {
static let shared = DashboardManager()
private var controller: DashboardWindowController?
private static let failureURL = URL(string: "about:blank")!
private init() {}
@discardableResult
func showConfiguredWindowIfPossible() -> Bool {
let mode = AppStateStore.shared.connectionMode
guard let config = self.immediateDashboardConfig(mode: mode),
let url = try? GatewayEndpointStore.dashboardURL(
for: config,
mode: mode,
authToken: config.token)
else {
return false
}
let auth = DashboardWindowAuth(
gatewayUrl: Self.websocketURLString(for: url),
token: config.token,
password: config.password?.trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty)
guard auth.hasCredential else {
return false
}
if let controller {
controller.show(url: url, auth: auth)
} else {
let controller = DashboardWindowController(url: url, auth: auth)
self.controller = controller
controller.show(url: url, auth: auth)
}
Task { _ = try? await ControlChannel.shared.health(timeout: 3) }
return true
}
func show() async throws {
let mode = AppStateStore.shared.connectionMode
dashboardManagerLogger.info("dashboard show requested mode=\(String(describing: mode), privacy: .public)")
let config = try await self.dashboardConfig(mode: mode)
dashboardManagerLogger.info("dashboard config url=\(config.url.absoluteString, privacy: .public)")
let token = await GatewayConnection.shared.controlUiAutoAuthToken(config: config)
let url = try GatewayEndpointStore.dashboardURL(for: config, mode: mode, authToken: token)
let auth = DashboardWindowAuth(
gatewayUrl: Self.websocketURLString(for: url),
token: token,
password: config.password?.trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty)
if let controller {
dashboardManagerLogger.info("dashboard reuse window url=\(url.absoluteString, privacy: .public)")
controller.show(url: url, auth: auth)
return
}
dashboardManagerLogger.info("dashboard create window url=\(url.absoluteString, privacy: .public)")
let controller = DashboardWindowController(url: url, auth: auth)
self.controller = controller
controller.show(url: url, auth: auth)
// Refresh the cached hello payload without blocking window creation.
Task { _ = try? await ControlChannel.shared.health(timeout: 3) }
}
func showFailure(_ error: Error) {
let message = (error as NSError).localizedDescription
dashboardManagerLogger.error("dashboard setup failed error=\(message, privacy: .public)")
let controller = self.controller ?? DashboardWindowController(
url: Self.failureURL,
auth: DashboardWindowAuth(gatewayUrl: nil, token: nil, password: nil))
self.controller = controller
controller.showFailure(
title: "Dashboard unavailable",
message: message,
detail: "Check Settings → Connection or use Debug → Reset Remote Tunnel, then try again.")
}
func close() {
self.controller?.closeDashboard()
}
private static func websocketURLString(for dashboardURL: URL) -> String {
guard var components = URLComponents(url: dashboardURL, resolvingAgainstBaseURL: false) else {
return dashboardURL.absoluteString
}
switch components.scheme?.lowercased() {
case "https":
components.scheme = "wss"
default:
components.scheme = "ws"
}
components.queryItems = nil
components.fragment = nil
return components.url?.absoluteString ?? dashboardURL.absoluteString
}
private func dashboardConfig(mode: AppState.ConnectionMode) async throws -> GatewayConnection.Config {
if let config = self.immediateDashboardConfig(mode: mode) {
return config
}
return try await Task.detached(priority: .userInitiated) {
await GatewayEndpointStore.shared.refresh()
return try await GatewayEndpointStore.shared.requireConfig()
}.value
}
private func immediateDashboardConfig(mode: AppState.ConnectionMode) -> GatewayConnection.Config? {
let root = OpenClawConfigFile.loadDict()
let resolution = GatewayRemoteConfig.resolveTransportResolution(root: root)
if mode == .remote,
resolution.transport == .direct,
let url = resolution.directURL
{
return (
url,
GatewayRemoteConfig.resolveTokenString(root: root),
GatewayRemoteConfig.resolvePasswordString(root: root))
}
if mode == .local {
return GatewayEndpointStore.localConfig()
}
return nil
}
}

View File

@@ -0,0 +1,21 @@
import AppKit
import Foundation
import OSLog
let dashboardWindowLogger = Logger(subsystem: "ai.openclaw", category: "DashboardWindow")
enum DashboardWindowLayout {
static let windowSize = NSSize(width: 1240, height: 860)
static let windowMinSize = NSSize(width: 900, height: 620)
}
struct DashboardWindowAuth: Equatable {
var gatewayUrl: String?
var token: String?
var password: String?
var hasCredential: Bool {
self.token?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false ||
self.password?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false
}
}

View File

@@ -0,0 +1,411 @@
import AppKit
import Foundation
import WebKit
private final class DashboardWindowContentView: NSView {
override var mouseDownCanMoveWindow: Bool {
true
}
}
private final class DashboardWindowDragRegionView: NSView {
override var mouseDownCanMoveWindow: Bool {
true
}
override func mouseDown(with event: NSEvent) {
self.window?.performDrag(with: event)
}
}
@MainActor
final class DashboardWindowController: NSWindowController, WKNavigationDelegate, NSWindowDelegate {
private let webView: WKWebView
private var currentURL: URL
private var auth: DashboardWindowAuth
init(url: URL, auth: DashboardWindowAuth) {
self.currentURL = url
self.auth = auth
let config = WKWebViewConfiguration()
config.preferences.isElementFullscreenEnabled = true
config.preferences.setValue(true, forKey: "developerExtrasEnabled")
config.userContentController = WKUserContentController()
Self.installNativeChromeScript(into: config.userContentController)
Self.installNativeAuthScript(into: config.userContentController, url: url, auth: auth)
self.webView = WKWebView(
frame: NSRect(origin: .zero, size: DashboardWindowLayout.windowSize),
configuration: config)
self.webView.setValue(true, forKey: "drawsBackground")
let window = Self.makeWindow(contentView: self.webView)
super.init(window: window)
self.webView.navigationDelegate = self
self.window?.delegate = self
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) is not supported")
}
func show(url: URL, auth: DashboardWindowAuth) {
self.currentURL = url
self.auth = auth
self.refreshNativeAuthScript(url: url, auth: auth)
self.load(url)
self.show()
}
func show() {
if let window {
let frame = window.frame
if frame.width < DashboardWindowLayout.windowMinSize.width ||
frame.height < DashboardWindowLayout.windowMinSize.height
{
window.setFrame(WindowPlacement.centeredFrame(size: DashboardWindowLayout.windowSize), display: false)
}
}
self.showWindow(nil)
self.window?.makeKeyAndOrderFront(nil)
self.window?.makeFirstResponder(self.webView)
self.window?.orderFrontRegardless()
NSApp.activate(ignoringOtherApps: true)
}
func closeDashboard() {
self.window?.performClose(nil)
}
func showFailure(title: String, message: String, detail: String? = nil) {
self.currentURL = URL(string: "about:blank")!
self.auth = DashboardWindowAuth(gatewayUrl: nil, token: nil, password: nil)
self.refreshNativeAuthScript(url: self.currentURL, auth: self.auth)
self.webView.stopLoading()
self.webView.loadHTMLString(
Self.failureHTML(title: title, message: message, detail: detail, url: nil),
baseURL: nil)
self.show()
}
private func load(_ url: URL) {
dashboardWindowLogger.debug("dashboard load \(url.absoluteString, privacy: .public)")
self.webView.load(URLRequest(url: url))
}
private func refreshNativeAuthScript(url: URL, auth: DashboardWindowAuth) {
let controller = self.webView.configuration.userContentController
controller.removeAllUserScripts()
Self.installNativeChromeScript(into: controller)
Self.installNativeAuthScript(into: controller, url: url, auth: auth)
}
private static func makeWindow(contentView: NSView) -> NSWindow {
let window = NSWindow(
contentRect: NSRect(origin: .zero, size: DashboardWindowLayout.windowSize),
styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
backing: .buffered,
defer: false)
let container = DashboardWindowContentView(frame: NSRect(origin: .zero, size: DashboardWindowLayout.windowSize))
contentView.translatesAutoresizingMaskIntoConstraints = false
container.addSubview(contentView)
let topDragRegion = DashboardWindowDragRegionView()
topDragRegion.translatesAutoresizingMaskIntoConstraints = false
container.addSubview(topDragRegion)
let topRightDragRegion = DashboardWindowDragRegionView()
topRightDragRegion.translatesAutoresizingMaskIntoConstraints = false
container.addSubview(topRightDragRegion)
let sidebarDragRegion = DashboardWindowDragRegionView()
sidebarDragRegion.translatesAutoresizingMaskIntoConstraints = false
container.addSubview(sidebarDragRegion)
NSLayoutConstraint.activate([
contentView.leadingAnchor.constraint(equalTo: container.leadingAnchor),
contentView.trailingAnchor.constraint(equalTo: container.trailingAnchor),
contentView.topAnchor.constraint(equalTo: container.topAnchor),
contentView.bottomAnchor.constraint(equalTo: container.bottomAnchor),
topDragRegion.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 78),
topDragRegion.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: -380),
topDragRegion.topAnchor.constraint(equalTo: container.topAnchor),
topDragRegion.heightAnchor.constraint(equalToConstant: 28),
topRightDragRegion.leadingAnchor.constraint(equalTo: topDragRegion.trailingAnchor),
topRightDragRegion.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: -8),
topRightDragRegion.topAnchor.constraint(equalTo: container.topAnchor),
topRightDragRegion.heightAnchor.constraint(equalToConstant: 6),
sidebarDragRegion.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 78),
sidebarDragRegion.topAnchor.constraint(equalTo: container.topAnchor),
sidebarDragRegion.widthAnchor.constraint(equalToConstant: 176),
sidebarDragRegion.heightAnchor.constraint(equalToConstant: 46),
])
window.title = "OpenClaw"
window.titleVisibility = .hidden
window.titlebarAppearsTransparent = true
window.isMovableByWindowBackground = true
window.isReleasedWhenClosed = false
window.hasShadow = true
window.backgroundColor = .windowBackgroundColor
window.isOpaque = true
let viewController = NSViewController()
viewController.view = container
window.contentViewController = viewController
window.center()
window.minSize = DashboardWindowLayout.windowMinSize
WindowPlacement.ensureOnScreen(window: window, defaultSize: DashboardWindowLayout.windowSize)
return window
}
private static func installNativeChromeScript(into userContentController: WKUserContentController) {
let css = """
html.openclaw-native-macos {
--openclaw-native-titlebar-height: 50px;
}
@media (min-width: 700px) {
html.openclaw-native-macos .sidebar-shell {
padding-top: max(14px, var(--openclaw-native-titlebar-height)) !important;
}
html.openclaw-native-macos .sidebar-shell__header {
padding-left: 10px !important;
padding-right: 8px !important;
}
}
"""
let script = """
(() => {
try {
if (document.getElementById("openclaw-native-macos-chrome")) return;
const style = document.createElement("style");
style.id = "openclaw-native-macos-chrome";
style.textContent = \(Self.jsStringLiteral(css));
document.documentElement.classList.add("openclaw-native-macos");
document.head.appendChild(style);
} catch {}
})();
"""
userContentController.addUserScript(
WKUserScript(source: script, injectionTime: .atDocumentEnd, forMainFrameOnly: true))
}
private static func installNativeAuthScript(
into userContentController: WKUserContentController,
url: URL,
auth: DashboardWindowAuth)
{
guard auth.hasCredential else { return }
let allowedOrigin = self.originString(for: url)
let allowedPath = self.allowedPath(for: url)
let payload: [String: Any?] = [
"gatewayUrl": auth.gatewayUrl,
"token": auth.token,
"password": auth.password,
]
guard let data = try? JSONSerialization.data(withJSONObject: payload.compactMapValues { $0 }),
let json = String(data: data, encoding: .utf8)
else {
return
}
let script = """
(() => {
try {
const allowedOrigin = \(Self.jsStringLiteral(allowedOrigin));
const allowedPath = \(Self.jsStringLiteral(allowedPath));
if (location.origin !== allowedOrigin) return;
if (allowedPath !== "/" && !location.pathname.startsWith(allowedPath)) return;
Object.defineProperty(window, "__OPENCLAW_NATIVE_CONTROL_AUTH__", {
value: \(json),
configurable: true,
});
} catch {}
})();
"""
userContentController.addUserScript(
WKUserScript(source: script, injectionTime: .atDocumentStart, forMainFrameOnly: true))
}
static func originString(for url: URL) -> String {
guard let scheme = url.scheme, let host = url.host else { return "" }
let hostPart = host.contains(":") && !host.hasPrefix("[") ? "[\(host)]" : host
var out = "\(scheme)://\(hostPart)"
if let port = url.port {
out += ":\(port)"
}
return out
}
private static func allowedPath(for url: URL) -> String {
let path = url.path.trimmingCharacters(in: .whitespacesAndNewlines)
guard !path.isEmpty else { return "/" }
return path.hasSuffix("/") ? path : path + "/"
}
private static func jsStringLiteral(_ value: String) -> String {
guard let data = try? JSONSerialization.data(withJSONObject: [value]),
let raw = String(data: data, encoding: .utf8),
raw.hasPrefix("["),
raw.hasSuffix("]")
else {
return "\"\""
}
return String(raw.dropFirst().dropLast())
}
func webView(
_: WKWebView,
decidePolicyFor navigationAction: WKNavigationAction,
decisionHandler: @escaping @MainActor @Sendable (WKNavigationActionPolicy) -> Void)
{
guard let url = navigationAction.request.url else {
decisionHandler(.allow)
return
}
if Self.shouldAllowNavigation(to: url, dashboardURL: self.currentURL) {
decisionHandler(.allow)
return
}
NSWorkspace.shared.open(url)
decisionHandler(.cancel)
}
func webView(_: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
self.showLoadFailure(error)
}
func webView(_: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
self.showLoadFailure(error)
}
static func shouldAllowNavigation(to url: URL, dashboardURL: URL) -> Bool {
guard let scheme = url.scheme?.lowercased() else { return true }
if scheme == "about" || scheme == "blob" || scheme == "data" { return true }
guard scheme == "http" || scheme == "https" else { return false }
return url.scheme?.lowercased() == dashboardURL.scheme?.lowercased() &&
url.host?.lowercased() == dashboardURL.host?.lowercased() &&
url.port == dashboardURL.port
}
func windowWillClose(_: Notification) {
self.webView.stopLoading()
}
private func showLoadFailure(_ error: Error) {
let nsError = error as NSError
if nsError.domain == NSURLErrorDomain, nsError.code == NSURLErrorCancelled { return }
dashboardWindowLogger.error(
"dashboard load failed url=\(self.currentURL.absoluteString, privacy: .public) error=\(error.localizedDescription, privacy: .public)")
let html = Self.failureHTML(
title: "Dashboard unavailable",
message: error.localizedDescription,
detail: "The dashboard window is open, but the web UI could not load from this endpoint.",
url: self.currentURL)
self.webView.loadHTMLString(html, baseURL: nil)
}
private static func failureHTML(title: String, message: String, detail: String?, url: URL?) -> String {
let detailHTML = detail.map { "<p class=\"detail\">\(self.htmlEscape($0))</p>" } ?? ""
let urlHTML = url.map { "<code>\(self.htmlEscape($0.absoluteString))</code>" } ?? ""
return """
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<style>
:root { color-scheme: light dark; }
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
display: grid;
place-items: center;
background: #101114;
color: rgba(255,255,255,.92);
font: 15px -apple-system, BlinkMacSystemFont, "SF Pro Text", system-ui, sans-serif;
}
main {
width: min(540px, calc(100vw - 72px));
padding: 34px;
border: 1px solid rgba(255,255,255,.12);
border-radius: 22px;
background: rgba(255,255,255,.035);
box-shadow: 0 28px 90px rgba(0,0,0,.36);
line-height: 1.45;
}
.badge {
width: 44px;
height: 44px;
display: grid;
place-items: center;
margin-bottom: 20px;
border-radius: 14px;
background: rgba(255,255,255,.07);
color: #ff746b;
font-size: 24px;
}
h1 {
margin: 0 0 12px;
font-size: 24px;
line-height: 1.16;
font-weight: 700;
letter-spacing: 0;
}
p {
margin: 0;
color: rgba(255,255,255,.76);
font-size: 16px;
}
.detail {
margin-top: 14px;
color: rgba(255,255,255,.56);
font-size: 13px;
}
code {
display: block;
margin-top: 18px;
padding: 12px;
border: 1px solid rgba(255,255,255,.08);
border-radius: 10px;
background: rgba(0,0,0,.26);
color: rgba(255,255,255,.76);
overflow-wrap: anywhere;
font: 12px ui-monospace, SFMono-Regular, Menlo, monospace;
}
@media (prefers-color-scheme: light) {
body { background: #f5f6f8; color: rgba(0,0,0,.86); }
main {
background: rgba(255,255,255,.84);
border-color: rgba(0,0,0,.1);
box-shadow: 0 28px 90px rgba(0,0,0,.12);
}
.badge { background: rgba(0,0,0,.06); }
p { color: rgba(0,0,0,.68); }
.detail { color: rgba(0,0,0,.54); }
code {
background: rgba(0,0,0,.05);
border-color: rgba(0,0,0,.08);
color: rgba(0,0,0,.68);
}
}
</style>
</head>
<body>
<main>
<div class="badge">!</div>
<h1>\(self.htmlEscape(title))</h1>
<p>\(self.htmlEscape(message))</p>
\(detailHTML)
\(urlHTML)
</main>
</body>
</html>
"""
}
private static func htmlEscape(_ value: String) -> String {
value
.replacingOccurrences(of: "&", with: "&amp;")
.replacingOccurrences(of: "<", with: "&lt;")
.replacingOccurrences(of: ">", with: "&gt;")
.replacingOccurrences(of: "\"", with: "&quot;")
.replacingOccurrences(of: "'", with: "&#39;")
}
}

View File

@@ -49,6 +49,7 @@ struct DebugSettings: View {
VStack(alignment: .leading, spacing: 14) {
self.header
self.overviewSection
self.launchdSection
self.appInfoSection
self.gatewaySection
@@ -62,8 +63,8 @@ struct DebugSettings: View {
Spacer(minLength: 0)
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, 24)
.padding(.vertical, 18)
.padding(.vertical, 4)
.padding(.trailing, SettingsLayout.scrollbarGutter)
.groupBoxStyle(PlainSettingsGroupBoxStyle())
}
.task {
@@ -119,6 +120,31 @@ struct DebugSettings: View {
}
}
private var overviewSection: some View {
HStack(spacing: 12) {
DebugMetricCard(
title: "App Health",
value: self.healthStore.state.debugTitle,
icon: "heart.text.square",
tint: self.healthStore.state.tint,
subtitle: self.healthStore.summaryLine)
DebugMetricCard(
title: "Gateway",
value: self.gatewayManager.status.label,
icon: "antenna.radiowaves.left.and.right",
tint: self.gatewayManager.status.debugTint,
subtitle: self.canRestartGateway ? "Local process" : "Remote connection")
DebugMetricCard(
title: "App PID",
value: "\(ProcessInfo.processInfo.processIdentifier)",
icon: "number.square",
tint: .blue,
subtitle: Bundle.main.bundleURL.lastPathComponent)
}
}
private func gridLabel(_ text: String) -> some View {
Text(text)
.foregroundStyle(.secondary)
@@ -216,8 +242,12 @@ struct DebugSettings: View {
.frame(maxWidth: .infinity, alignment: .leading)
.textSelection(.enabled)
}
.frame(height: 180)
.overlay(RoundedRectangle(cornerRadius: 6).stroke(Color.secondary.opacity(0.2)))
.frame(height: 130)
.background(.black.opacity(0.12), in: RoundedRectangle(cornerRadius: 8, style: .continuous))
.overlay {
RoundedRectangle(cornerRadius: 8, style: .continuous)
.strokeBorder(.white.opacity(0.06))
}
HStack(spacing: 8) {
if self.canRestartGateway {
@@ -929,13 +959,81 @@ extension DebugSettings {
struct PlainSettingsGroupBoxStyle: GroupBoxStyle {
func makeBody(configuration: Configuration) -> some View {
VStack(alignment: .leading, spacing: 10) {
VStack(alignment: .leading, spacing: 12) {
configuration.label
.font(.caption.weight(.semibold))
.font(.subheadline.weight(.semibold))
.foregroundStyle(.secondary)
configuration.content
}
.padding(14)
.frame(maxWidth: .infinity, alignment: .leading)
.background(.quaternary.opacity(0.34), in: RoundedRectangle(cornerRadius: 12, style: .continuous))
.overlay {
RoundedRectangle(cornerRadius: 12, style: .continuous)
.strokeBorder(.white.opacity(0.055))
}
}
}
private struct DebugMetricCard: View {
let title: String
let value: String
let icon: String
let tint: Color
let subtitle: String
var body: some View {
HStack(alignment: .top, spacing: 12) {
Image(systemName: self.icon)
.font(.system(size: 18, weight: .semibold))
.foregroundStyle(self.tint)
.frame(width: 34, height: 34)
.background(self.tint.opacity(0.18), in: Circle())
VStack(alignment: .leading, spacing: 3) {
Text(self.title)
.font(.caption.weight(.semibold))
.foregroundStyle(.secondary)
Text(self.value)
.font(.callout.weight(.semibold))
.lineLimit(1)
.truncationMode(.tail)
Text(self.subtitle)
.font(.caption2)
.foregroundStyle(.secondary)
.lineLimit(2)
.fixedSize(horizontal: false, vertical: true)
}
}
.padding(12)
.frame(maxWidth: .infinity, alignment: .topLeading)
.background(.quaternary.opacity(0.28), in: RoundedRectangle(cornerRadius: 12, style: .continuous))
.overlay {
RoundedRectangle(cornerRadius: 12, style: .continuous)
.strokeBorder(.white.opacity(0.055))
}
}
}
extension HealthState {
fileprivate var debugTitle: String {
switch self {
case .unknown: "Unknown"
case .ok: "Healthy"
case .linkingNeeded: "Needs Link"
case .degraded: "Degraded"
}
}
}
extension GatewayProcessManager.Status {
fileprivate var debugTint: Color {
switch self {
case .running, .attachedExisting: .green
case .starting: .orange
case .failed: .red
case .stopped: .secondary
}
}
}
@@ -984,6 +1082,7 @@ extension DebugSettings {
_ = view.body
_ = view.header
_ = view.overviewSection
_ = view.appInfoSection
_ = view.gatewaySection
_ = view.logsSection

View File

@@ -69,6 +69,8 @@ final class DeepLinkHandler {
await self.handleAgent(link: link, originalURL: url)
case .gateway:
break
case .dashboard:
await self.openDashboard()
}
}
@@ -178,6 +180,14 @@ final class DeepLinkHandler {
// MARK: - UI
private func openDashboard() async {
do {
try await DashboardManager.shared.show()
} catch {
DashboardManager.shared.showFailure(error)
}
}
private func confirm(title: String, message: String) -> Bool {
let alert = NSAlert()
alert.messageText = title

View File

@@ -33,7 +33,7 @@ final class DevicePairingApprovalPrompter {
let remoteIp: String?
}
private struct PendingRequest: Codable, Equatable, Identifiable {
struct PendingRequest: Codable, Equatable, Identifiable {
let requestId: String
let deviceId: String
let publicKey: String
@@ -115,14 +115,16 @@ final class DevicePairingApprovalPrompter {
PairingAlertSupport.presentPairingAlert(
request: req,
requestId: req.requestId,
messageText: "Allow device to connect?",
informativeText: Self.describe(req),
messageText: Self.alertTitle(for: req),
informativeText: Self.alertSummary(for: req),
buttonTitles: PairingAlertSupport.ButtonTitles(approve: Self.approveButtonTitle(for: req)),
accessoryView: Self.buildAccessoryView(for: req),
state: self.alertState,
onResponse: self.handleAlertResponse)
}
private func handleAlertResponse(_ response: NSApplication.ModalResponse, request: PendingRequest) async {
var shouldRemove = response != .alertFirstButtonReturn
var shouldRemove = response != .alertSecondButtonReturn
defer {
if shouldRemove {
if self.queue.first == request {
@@ -144,14 +146,14 @@ final class DevicePairingApprovalPrompter {
switch response {
case .alertFirstButtonReturn:
_ = await self.approve(requestId: request.requestId)
case .alertSecondButtonReturn:
shouldRemove = false
if let idx = self.queue.firstIndex(of: request) {
self.queue.remove(at: idx)
}
self.queue.append(request)
return
case .alertSecondButtonReturn:
_ = await self.approve(requestId: request.requestId)
case .alertThirdButtonReturn:
await self.reject(requestId: request.requestId)
default:
@@ -233,24 +235,166 @@ final class DevicePairingApprovalPrompter {
self.updatePendingCounts()
}
private static func describe(_ req: PendingRequest) -> String {
var lines: [String] = []
lines.append("Device: \(req.displayName ?? req.deviceId)")
if let platform = req.platform {
lines.append("Platform: \(platform)")
static func alertTitle(for req: PendingRequest) -> String {
self.isMac(req.platform) ? "New Mac wants to connect" : "New device wants to connect"
}
static func alertSummary(for req: PendingRequest) -> String {
let subject = self.isMac(req.platform) ? "this Mac app" : "this device"
return "Approve \(subject) to control OpenClaw. Only approve if this is yours; you can remove it later in Settings."
}
static func approveButtonTitle(for req: PendingRequest) -> String {
self.isMac(req.platform) ? "Approve Mac" : "Approve Device"
}
static func buildAccessoryView(for req: PendingRequest) -> NSView {
let stack = NSStackView()
stack.orientation = .vertical
stack.alignment = .leading
stack.spacing = 8
stack.edgeInsets = NSEdgeInsets(top: 2, left: 0, bottom: 0, right: 0)
stack.addArrangedSubview(self.makeValueRow(label: "Device", value: self.deviceName(for: req)))
if let platform = self.prettyPlatform(req.platform) {
stack.addArrangedSubview(self.makeValueRow(label: "Platform", value: platform))
}
if let role = req.role {
lines.append("Role: \(role)")
if let role = self.prettyRole(req.role) {
stack.addArrangedSubview(self.makeValueRow(label: "Role", value: role))
}
if let scopes = req.scopes, !scopes.isEmpty {
lines.append("Scopes: \(scopes.joined(separator: ", "))")
let accessItems = self.friendlyScopeNames(req.scopes)
if !accessItems.isEmpty {
stack.addArrangedSubview(self.makeSectionLabel("Access requested"))
for item in accessItems {
stack.addArrangedSubview(self.makeBullet(item))
}
}
if let remoteIp = req.remoteIp {
lines.append("IP: \(remoteIp)")
stack.addArrangedSubview(self.makeDetailLine(req))
let fitting = stack.fittingSize
stack.frame = NSRect(x: 0, y: 0, width: 420, height: fitting.height)
return stack
}
static func deviceName(for req: PendingRequest) -> String {
let trimmedName = req.displayName?.trimmingCharacters(in: .whitespacesAndNewlines)
if let trimmedName, !trimmedName.isEmpty, trimmedName != req.deviceId {
return trimmedName
}
return self.isMac(req.platform) ? "OpenClaw Mac app" : "New device"
}
static func prettyPlatform(_ raw: String?) -> String? {
let platform = raw?.trimmingCharacters(in: .whitespacesAndNewlines)
guard let platform, !platform.isEmpty else { return nil }
switch platform.lowercased() {
case "macintel", "x86_64-apple-darwin":
return "Mac (Intel)"
case "macarm", "macarm64", "arm64-apple-darwin", "aarch64-apple-darwin":
return "Mac (Apple silicon)"
case "darwin":
return "Mac"
default:
if platform.lowercased().contains("mac") {
return "Mac"
}
return platform
}
}
static func prettyRole(_ raw: String?) -> String? {
let role = raw?.trimmingCharacters(in: .whitespacesAndNewlines)
guard let role, !role.isEmpty else { return nil }
return role == "operator" ? "Operator" : role
}
static func friendlyScopeNames(_ scopes: [String]?) -> [String] {
guard let scopes else { return [] }
var seen = Set<String>()
return scopes.compactMap { scope in
let normalized = scope.trimmingCharacters(in: .whitespacesAndNewlines)
guard !normalized.isEmpty, seen.insert(normalized).inserted else { return nil }
switch normalized {
case "operator.admin":
return "Admin access"
case "operator.read":
return "Read OpenClaw data"
case "operator.write":
return "Send messages and make changes"
case "operator.approvals":
return "Manage approvals"
case "operator.pairing":
return "Pair and repair devices"
case "operator.talk.secrets":
return "Use Talk credentials"
default:
return normalized
}
}
}
static func shortIdentifier(_ id: String) -> String {
let trimmed = id.trimmingCharacters(in: .whitespacesAndNewlines)
guard trimmed.count > 20 else { return trimmed }
return "\(trimmed.prefix(8))...\(trimmed.suffix(7))"
}
private static func isMac(_ platform: String?) -> Bool {
guard let platform else { return false }
let lower = platform.lowercased()
return lower.contains("mac") || lower.contains("darwin")
}
private static func makeValueRow(label: String, value: String) -> NSView {
let row = NSStackView()
row.orientation = .horizontal
row.alignment = .firstBaseline
row.spacing = 8
let labelField = self.makeLabel("\(label):", font: .systemFont(ofSize: 12, weight: .semibold))
labelField.textColor = .secondaryLabelColor
labelField.setContentHuggingPriority(.required, for: .horizontal)
let valueField = self.makeLabel(value, font: .systemFont(ofSize: 12, weight: .regular))
valueField.maximumNumberOfLines = 2
row.addArrangedSubview(labelField)
row.addArrangedSubview(valueField)
return row
}
private static func makeSectionLabel(_ text: String) -> NSTextField {
let label = self.makeLabel(text, font: .systemFont(ofSize: 12, weight: .semibold))
label.textColor = .secondaryLabelColor
return label
}
private static func makeBullet(_ text: String) -> NSTextField {
let label = self.makeLabel("\(text)", font: .systemFont(ofSize: 12, weight: .regular))
label.maximumNumberOfLines = 2
return label
}
private static func makeDetailLine(_ req: PendingRequest) -> NSTextField {
var parts = ["ID \(self.shortIdentifier(req.deviceId))"]
if let remoteIp = req.remoteIp?.trimmingCharacters(in: .whitespacesAndNewlines), !remoteIp.isEmpty {
parts.append("IP \(remoteIp.replacingOccurrences(of: "::ffff:", with: ""))")
}
if req.isRepair == true {
lines.append("Repair: yes")
parts.append("repair request")
}
return lines.joined(separator: "\n")
let label = self.makeLabel(
parts.joined(separator: " · "),
font: .monospacedSystemFont(ofSize: 11, weight: .regular))
label.textColor = .tertiaryLabelColor
label.maximumNumberOfLines = 2
return label
}
private static func makeLabel(_ text: String, font: NSFont) -> NSTextField {
let label = NSTextField(labelWithString: text)
label.font = font
label.lineBreakMode = .byWordWrapping
label.textColor = .labelColor
return label
}
}

View File

@@ -28,7 +28,7 @@ final class DockIconManager: NSObject, @unchecked Sendable {
return
}
let userWantsDockHidden = !UserDefaults.standard.bool(forKey: showDockIconKey)
let userWantsDockHidden = (UserDefaults.standard.object(forKey: showDockIconKey) as? Bool) == false
let visibleWindows = NSApp?.windows.filter { window in
window.isVisible &&
window.frame.width > 1 &&

View File

@@ -64,6 +64,7 @@ actor GatewayConnection {
case configSet = "config.set"
case configPatch = "config.patch"
case configSchema = "config.schema"
case configSchemaLookup = "config.schema.lookup"
case wizardStart = "wizard.start"
case wizardNext = "wizard.next"
case wizardCancel = "wizard.cancel"
@@ -317,6 +318,28 @@ actor GatewayConnection {
return trimmed.isEmpty ? nil : trimmed
}
func controlUiAutoAuthToken(config: Config) async -> String? {
if let token = config.token?.trimmingCharacters(in: .whitespacesAndNewlines),
!token.isEmpty
{
return token
}
if let deviceToken = self.lastSnapshot?.auth["deviceToken"]?.value as? String {
let trimmed = deviceToken.trimmingCharacters(in: .whitespacesAndNewlines)
if !trimmed.isEmpty {
return trimmed
}
}
let identity = DeviceIdentityStore.loadOrCreate()
if let entry = DeviceAuthStore.loadToken(deviceId: identity.deviceId, role: "operator") {
let trimmed = entry.token.trimmingCharacters(in: .whitespacesAndNewlines)
if !trimmed.isEmpty {
return trimmed
}
}
return nil
}
private func sessionDefaultString(_ defaults: [String: OpenClawProtocol.AnyCodable]?, key: String) -> String {
let raw = defaults?[key]?.value as? String
return (raw ?? "").trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)

View File

@@ -41,21 +41,31 @@ enum GatewayDiscoveryHelpers {
static func directUrl(for gateway: GatewayDiscoveryModel.DiscoveredGateway) -> String? {
self.directGatewayUrl(
serviceHost: gateway.serviceHost,
servicePort: gateway.servicePort)
servicePort: gateway.servicePort,
gatewayTls: gateway.gatewayTls)
}
static func directGatewayUrl(
serviceHost: String?,
servicePort: Int?) -> String?
servicePort: Int?,
gatewayTls: Bool = false) -> String?
{
// Security: do not route using unauthenticated TXT hints (tailnetDns/lanHost/gatewayPort).
// Prefer the resolved service endpoint (SRV + A/AAAA).
guard let endpoint = self.serviceEndpoint(serviceHost: serviceHost, servicePort: servicePort) else {
return nil
}
// Security: for non-loopback hosts, force TLS to avoid plaintext credential/session leakage.
let scheme = self.isLoopbackHost(endpoint.host) ? "ws" : "wss"
let portSuffix = endpoint.port == 443 ? "" : ":\(endpoint.port)"
let scheme: String
if gatewayTls {
scheme = "wss"
} else if self.isLoopbackHost(endpoint.host)
|| GatewayRemoteConfig.isTrustedPlaintextRemoteHost(endpoint.host)
{
scheme = "ws"
} else {
return nil
}
let portSuffix = scheme == "wss" && endpoint.port == 443 ? "" : ":\(endpoint.port)"
return "\(scheme)://\(endpoint.host)\(portSuffix)"
}

View File

@@ -25,14 +25,14 @@ enum GatewayDiscoverySelectionSupport {
state.remoteTarget = GatewayDiscoveryHelpers.sshTarget(for: gateway) ?? ""
if preferredTransport == .direct {
if let endpoint = GatewayDiscoveryHelpers.serviceEndpoint(for: gateway) {
OpenClawConfigFile.setRemoteGatewayUrl(
host: endpoint.host,
port: endpoint.port)
OpenClawConfigFile.setRemoteGatewayTransport(AppState.RemoteTransport.direct.rawValue)
if !state.remoteUrl.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
OpenClawConfigFile.setRemoteGatewayUrlString(state.remoteUrl)
} else {
OpenClawConfigFile.clearRemoteGatewayUrl()
}
} else {
OpenClawConfigFile.setRemoteGatewayTransport(AppState.RemoteTransport.ssh.rawValue)
OpenClawConfigFile.setRemoteGatewayUrlString(state.remoteUrl)
}
}
@@ -65,9 +65,10 @@ enum GatewayDiscoverySelectionSupport {
for gateway: GatewayDiscoveryModel.DiscoveredGateway) -> Bool
{
guard GatewayDiscoveryHelpers.directUrl(for: gateway) != nil else { return false }
if gateway.stableID.hasPrefix("tailscale-serve|") {
if gateway.gatewayTls || gateway.gatewayDirectReachable {
return true
}
guard let host = GatewayDiscoveryHelpers.resolvedServiceHost(for: gateway)?
.trimmingCharacters(in: .whitespacesAndNewlines)
.lowercased()

View File

@@ -306,8 +306,9 @@ actor GatewayEndpointStore {
password: password))
case .remote:
let root = OpenClawConfigFile.loadDict()
if GatewayRemoteConfig.resolveTransport(root: root) == .direct {
guard let url = GatewayRemoteConfig.resolveGatewayUrl(root: root) else {
let resolution = GatewayRemoteConfig.resolveTransportResolution(root: root)
if resolution.transport == .direct {
guard let url = resolution.directURL else {
self.cancelRemoteEnsure()
self.setState(.unavailable(
mode: .remote,
@@ -470,8 +471,9 @@ actor GatewayEndpointStore {
private func resolveDirectRemoteURL() throws -> URL? {
let root = OpenClawConfigFile.loadDict()
guard GatewayRemoteConfig.resolveTransport(root: root) == .direct else { return nil }
guard let url = GatewayRemoteConfig.resolveGatewayUrl(root: root) else {
let resolution = GatewayRemoteConfig.resolveTransportResolution(root: root)
guard resolution.transport == .direct else { return nil }
guard let url = resolution.directURL else {
throw NSError(
domain: "GatewayEndpoint",
code: 1,
@@ -667,7 +669,8 @@ extension GatewayEndpointStore {
static func dashboardURL(
for config: GatewayConnection.Config,
mode: AppState.ConnectionMode,
localBasePath: String? = nil) throws -> URL
localBasePath: String? = nil,
authToken: String? = nil) throws -> URL
{
guard var components = URLComponents(url: config.url, resolvingAgainstBaseURL: false) else {
throw NSError(domain: "Dashboard", code: 1, userInfo: [
@@ -694,7 +697,8 @@ extension GatewayEndpointStore {
}
var fragmentItems: [URLQueryItem] = []
if let token = config.token?.trimmingCharacters(in: .whitespacesAndNewlines),
let tokenCandidate = authToken ?? config.token
if let token = tokenCandidate?.trimmingCharacters(in: .whitespacesAndNewlines),
!token.isEmpty
{
fragmentItems.append(URLQueryItem(name: "token", value: token))

View File

@@ -1,7 +1,22 @@
import Foundation
import OpenClawKit
#if canImport(Darwin)
import Darwin
#endif
enum GatewayRemoteConfig {
enum TransportSource: Equatable {
case explicit
case inferredRemoteURL
case legacySSH
}
struct TransportResolution: Equatable {
let transport: AppState.RemoteTransport
let source: TransportSource
let directURL: URL?
}
enum TokenValue: Equatable {
case missing
case plaintext(String)
@@ -25,14 +40,49 @@ enum GatewayRemoteConfig {
}
static func resolveTransport(root: [String: Any]) -> AppState.RemoteTransport {
self.resolveTransportResolution(root: root).transport
}
static func resolveTransportResolution(root: [String: Any]) -> TransportResolution {
let explicit = self.resolveExplicitTransport(root: root)
switch explicit {
case .direct:
return TransportResolution(
transport: .direct,
source: .explicit,
directURL: self.resolveGatewayUrl(root: root))
case .ssh:
return TransportResolution(transport: .ssh, source: .explicit, directURL: nil)
case nil:
break
}
if let url = self.resolveGatewayUrl(root: root),
let host = url.host,
!LoopbackHost.isLoopbackHost(host)
{
return TransportResolution(transport: .direct, source: .inferredRemoteURL, directURL: url)
}
return TransportResolution(transport: .ssh, source: .legacySSH, directURL: nil)
}
private static func resolveExplicitTransport(root: [String: Any]) -> AppState.RemoteTransport? {
guard let gateway = root["gateway"] as? [String: Any],
let remote = gateway["remote"] as? [String: Any],
let raw = remote["transport"] as? String
else {
return .ssh
return nil
}
let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
return trimmed == AppState.RemoteTransport.direct.rawValue ? .direct : .ssh
switch trimmed {
case AppState.RemoteTransport.direct.rawValue:
return .direct
case AppState.RemoteTransport.ssh.rawValue:
return .ssh
default:
return .ssh
}
}
static func resolveUrlString(root: [String: Any]) -> String? {
@@ -69,6 +119,17 @@ enum GatewayRemoteConfig {
}
}
static func resolvePasswordString(root: [String: Any]) -> String? {
guard let gateway = root["gateway"] as? [String: Any],
let remote = gateway["remote"] as? [String: Any],
let raw = remote["password"] as? String
else {
return nil
}
let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
return trimmed.isEmpty ? nil : trimmed
}
static func resolveTLSFingerprint(root: [String: Any]) -> String? {
guard let gateway = root["gateway"] as? [String: Any],
let remote = gateway["remote"] as? [String: Any],
@@ -85,6 +146,27 @@ enum GatewayRemoteConfig {
return self.normalizeGatewayUrl(raw)
}
static func resolveRemotePort(root: [String: Any]) -> Int? {
guard let gateway = root["gateway"] as? [String: Any],
let remote = gateway["remote"] as? [String: Any]
else {
return nil
}
let value = remote["remotePort"]
let port: Int? = switch value {
case let raw as Int:
raw
case let raw as NSNumber:
raw.intValue
case let raw as String:
Int(raw.trimmingCharacters(in: .whitespacesAndNewlines))
default:
nil
}
guard let port, port > 0, port <= 65535 else { return nil }
return port
}
static func normalizeGatewayUrlString(_ raw: String) -> String? {
self.normalizeGatewayUrl(raw)?.absoluteString
}
@@ -96,7 +178,10 @@ enum GatewayRemoteConfig {
guard scheme == "ws" || scheme == "wss" else { return nil }
let host = url.host?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
guard !host.isEmpty else { return nil }
if scheme == "ws", !LoopbackHost.isLoopbackHost(host) {
if scheme == "ws",
!LoopbackHost.isLoopbackHost(host),
!self.isTrustedPlaintextRemoteHost(host)
{
return nil
}
if scheme == "ws", url.port == nil {
@@ -109,6 +194,59 @@ enum GatewayRemoteConfig {
return url
}
static func isTrustedPlaintextRemoteHost(_ host: String) -> Bool {
let lower = host.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
guard !lower.isEmpty else { return false }
if lower == "localhost" || lower.hasSuffix(".local") || lower.hasSuffix(".ts.net") {
return true
}
if self.isPrivateIPv6Literal(lower) {
return true
}
guard let parts = self.ipv4Parts(lower) else { return false }
switch (parts[0], parts[1]) {
case (10, _), (192, 168), (169, 254):
return true
case (172, 16...31):
return true
case (100, 64...127):
return true
default:
return false
}
}
private static func ipv4Parts(_ value: String) -> [Int]? {
let labels = value.split(separator: ".", omittingEmptySubsequences: false)
guard labels.count == 4 else { return nil }
var parts: [Int] = []
parts.reserveCapacity(4)
for label in labels {
guard !label.isEmpty,
label.allSatisfy(\.isNumber),
let part = Int(label),
part >= 0,
part <= 255
else {
return nil
}
parts.append(part)
}
return parts
}
private static func isPrivateIPv6Literal(_ value: String) -> Bool {
#if canImport(Darwin)
var addr = in6_addr()
guard value.withCString({ inet_pton(AF_INET6, $0, &addr) }) == 1 else {
return false
}
return value.hasPrefix("fc") || value.hasPrefix("fd") || value.hasPrefix("fe80:")
#else
return false
#endif
}
static func defaultPort(for url: URL) -> Int? {
if let port = url.port { return port }
let scheme = url.scheme?.lowercased() ?? ""

View File

@@ -6,8 +6,15 @@ import OpenClawKit
import SwiftUI
struct GeneralSettings: View {
enum Page {
case general
case connection
}
@Bindable var state: AppState
@AppStorage(cameraEnabledKey) private var cameraEnabled: Bool = false
let page: Page
let isActive: Bool
private let healthStore = HealthStore.shared
private let gatewayManager = GatewayProcessManager.shared
@State private var gatewayDiscovery = GatewayDiscoveryModel(
@@ -20,79 +27,177 @@ struct GeneralSettings: View {
ProcessInfo.processInfo.isNixMode
}
private var remoteLabelWidth: CGFloat {
88
init(state: AppState, page: Page = .general, isActive: Bool = true) {
self.state = state
self.page = page
self.isActive = isActive
}
var body: some View {
ScrollView(.vertical) {
VStack(alignment: .leading, spacing: 18) {
VStack(alignment: .leading, spacing: 12) {
SettingsToggleRow(
title: "OpenClaw active",
subtitle: "Pause to stop the OpenClaw gateway; no messages will be processed.",
binding: self.activeBinding)
self.connectionSection
Divider()
SettingsToggleRow(
title: "Launch at login",
subtitle: "Automatically start OpenClaw after you sign in.",
binding: self.$state.launchAtLogin)
SettingsToggleRow(
title: "Show Dock icon",
subtitle: "Keep OpenClaw visible in the Dock instead of menu-bar-only mode.",
binding: self.$state.showDockIcon)
SettingsToggleRow(
title: "Play menu bar icon animations",
subtitle: "Enable idle blinks and wiggles on the status icon.",
binding: self.$state.iconAnimationsEnabled)
SettingsToggleRow(
title: "Allow Canvas",
subtitle: "Allow the agent to show and control the Canvas panel.",
binding: self.$state.canvasEnabled)
SettingsToggleRow(
title: "Allow Camera",
subtitle: "Allow the agent to capture a photo or short video via the built-in camera.",
binding: self.$cameraEnabled)
SettingsToggleRow(
title: "Enable Peekaboo Bridge",
subtitle: "Allow signed tools (e.g. `peekaboo`) to drive UI automation via PeekabooBridge.",
binding: self.$state.peekabooBridgeEnabled)
SettingsToggleRow(
title: "Enable debug tools",
subtitle: "Show the Debug tab with development utilities.",
binding: self.$state.debugPaneEnabled)
}
Spacer(minLength: 12)
HStack {
Spacer()
Button("Quit OpenClaw") { NSApp.terminate(nil) }
.buttonStyle(.borderedProminent)
VStack(alignment: .leading, spacing: 20) {
switch self.page {
case .general:
self.generalPage
case .connection:
self.connectionPage
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, 22)
.frame(maxWidth: 760, alignment: .leading)
.padding(.bottom, 16)
.padding(.leading, 18)
.padding(.trailing, SettingsLayout.scrollbarGutter)
}
.onAppear {
guard !self.isPreview else { return }
self.refreshGatewayStatus()
self.updateActiveWork(active: self.isActive)
}
.onChange(of: self.isActive) { _, active in
self.updateActiveWork(active: active)
}
.onChange(of: self.state.canvasEnabled) { _, enabled in
if !enabled {
CanvasManager.shared.hideAll()
}
}
.onDisappear { self.gatewayDiscovery.stop() }
}
private var generalPage: some View {
VStack(alignment: .leading, spacing: 20) {
SettingsPageHeader(
title: "General",
subtitle: "Everyday OpenClaw app behavior.")
self.openClawStatusPanel
SettingsCardGroup("App") {
SettingsCardToggleRow(
title: "Launch at login",
subtitle: "Automatically start OpenClaw after you sign in.",
binding: self.$state.launchAtLogin)
SettingsCardToggleRow(
title: "Show Dock icon",
subtitle: "Keep OpenClaw visible in the Dock. When off, windows still show the Dock icon while open.",
binding: self.$state.showDockIcon)
SettingsCardToggleRow(
title: "Play menu bar icon animations",
subtitle: "Enable idle blinks and wiggles on the status icon.",
binding: self.$state.iconAnimationsEnabled,
showsDivider: false)
}
SettingsCardGroup("Capabilities") {
SettingsCardToggleRow(
title: "Allow Canvas",
subtitle: "Allow the agent to show and control the Canvas panel.",
binding: self.$state.canvasEnabled)
SettingsCardToggleRow(
title: "Allow Camera",
subtitle: "Allow the agent to capture a photo or short video via the built-in camera.",
binding: self.$cameraEnabled)
SettingsCardToggleRow(
title: "Enable Peekaboo Bridge",
subtitle: "Allow signed tools (e.g. `peekaboo`) to drive UI automation via PeekabooBridge.",
binding: self.$state.peekabooBridgeEnabled,
showsDivider: false)
}
SettingsCardGroup("Developer") {
SettingsCardToggleRow(
title: "Enable debug tools",
subtitle: "Show the Debug page with development utilities.",
binding: self.$state.debugPaneEnabled,
showsDivider: false)
}
HStack(alignment: .center, spacing: 12) {
VStack(alignment: .leading, spacing: 3) {
Text("App session")
.font(.callout.weight(.medium))
Text("Quit only when you want to stop the menu bar app completely.")
.font(.footnote)
.foregroundStyle(.secondary)
}
Spacer(minLength: 18)
Button("Quit") { NSApp.terminate(nil) }
.buttonStyle(.bordered)
.controlSize(.small)
}
.padding(.top, 2)
}
}
private var openClawStatusPanel: some View {
HStack(alignment: .center, spacing: 14) {
ZStack {
Circle()
.fill(self.state.isPaused ? Color.orange.opacity(0.18) : Color.green.opacity(0.18))
Image(systemName: self.state.isPaused ? "pause.fill" : "checkmark")
.font(.system(size: 16, weight: .bold))
.foregroundStyle(self.state.isPaused ? .orange : .green)
}
.frame(width: 42, height: 42)
VStack(alignment: .leading, spacing: 4) {
Text(self.state.isPaused ? "OpenClaw paused" : "OpenClaw active")
.font(.headline)
Text(self.generalStatusSubtitle)
.font(.footnote)
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
}
Spacer(minLength: 20)
Toggle("OpenClaw active", isOn: self.activeBinding)
.labelsHidden()
.toggleStyle(.switch)
}
.padding(.horizontal, 16)
.padding(.vertical, 14)
.background(.quaternary.opacity(0.45), in: RoundedRectangle(cornerRadius: 12, style: .continuous))
.overlay {
RoundedRectangle(cornerRadius: 12, style: .continuous)
.strokeBorder(.white.opacity(0.06))
}
}
private var generalStatusSubtitle: String {
if self.state.isPaused {
return "Gateway work is paused; incoming messages will wait."
}
switch self.state.connectionMode {
case .local:
return "Processing messages through the local Gateway on this Mac."
case .remote:
return "Connected to a remote Gateway configuration."
case .unconfigured:
return "Ready to run after you choose a Gateway connection."
}
}
private var connectionPage: some View {
VStack(alignment: .leading, spacing: 20) {
SettingsPageHeader(
title: "Connection",
subtitle: "Choose where the Gateway runs and how this Mac app reaches it.")
self.connectionStatusPanel
self.gatewayModeGroup
switch self.state.connectionMode {
case .unconfigured:
EmptyView()
case .local:
self.localGatewayGroup
case .remote:
self.remoteCard
}
}
}
private var activeBinding: Binding<Bool> {
@@ -101,56 +206,175 @@ struct GeneralSettings: View {
set: { self.state.isPaused = !$0 })
}
private var connectionSection: some View {
VStack(alignment: .leading, spacing: 10) {
Text("OpenClaw runs")
.font(.title3.weight(.semibold))
.frame(maxWidth: .infinity, alignment: .leading)
Picker("Mode", selection: self.$state.connectionMode) {
Text("Not configured").tag(AppState.ConnectionMode.unconfigured)
Text("Local (this Mac)").tag(AppState.ConnectionMode.local)
Text("Remote (another host)").tag(AppState.ConnectionMode.remote)
private func updateActiveWork(active: Bool) {
guard !self.isPreview else { return }
if active {
self.refreshGatewayStatus()
if self.page == .connection {
self.gatewayDiscovery.start()
}
.pickerStyle(.menu)
.labelsHidden()
.frame(width: 260, alignment: .leading)
} else {
self.gatewayDiscovery.stop()
}
}
if self.state.connectionMode == .unconfigured {
Text("Pick Local or Remote to start the Gateway.")
private var connectionStatusPanel: some View {
HStack(alignment: .center, spacing: 14) {
ZStack {
RoundedRectangle(cornerRadius: 16, style: .continuous)
.fill(self.connectionStatusTint.opacity(0.18))
Image(systemName: self.connectionStatusIcon)
.font(.system(size: 18, weight: .semibold))
.foregroundStyle(self.connectionStatusTint)
}
.frame(width: 46, height: 46)
VStack(alignment: .leading, spacing: 4) {
Text(self.connectionStatusTitle)
.font(.headline)
Text(self.connectionStatusSubtitle)
.font(.footnote)
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
}
if self.state.connectionMode == .local {
// In Nix mode, gateway is managed declaratively - no install buttons.
if !self.isNixMode {
self.gatewayInstallerCard
Spacer(minLength: 18)
if let ping = ControlChannel.shared.lastPingMs {
Text("\(Int(ping)) ms")
.font(.caption.weight(.semibold))
.padding(.horizontal, 8)
.padding(.vertical, 4)
.background(.green.opacity(0.16), in: Capsule())
.foregroundStyle(.green)
}
}
.padding(.horizontal, 16)
.padding(.vertical, 14)
.background(.quaternary.opacity(0.45), in: RoundedRectangle(cornerRadius: 12, style: .continuous))
.overlay {
RoundedRectangle(cornerRadius: 12, style: .continuous)
.strokeBorder(.white.opacity(0.06))
}
}
private var connectionStatusIcon: String {
switch self.state.connectionMode {
case .local: "desktopcomputer"
case .remote: self.state.remoteTransport == .ssh ? "point.3.connected.trianglepath.dotted" : "network"
case .unconfigured: "questionmark.circle"
}
}
private var connectionStatusTint: Color {
switch ControlChannel.shared.state {
case .connected: .green
case .connecting, .disconnected, .degraded: .orange
}
}
private var connectionStatusTitle: String {
switch self.state.connectionMode {
case .local: "Local Gateway"
case .remote: self.state.remoteTransport == .ssh ? "Remote Gateway via SSH" : "Remote Gateway direct"
case .unconfigured: "Gateway not configured"
}
}
private var connectionStatusSubtitle: String {
switch self.state.connectionMode {
case .local:
return "OpenClaw starts and monitors the Gateway on this Mac."
case .remote:
let target = self.state.remoteTransport == .ssh ? self.state.remoteTarget : self.state.remoteUrl
let trimmed = target.trimmingCharacters(in: .whitespacesAndNewlines)
if trimmed.isEmpty {
return "Enter a remote endpoint so this Mac app can attach cleanly."
}
return "\(self.controlStatusLine) · \(trimmed)"
case .unconfigured:
return "Choose local or remote before the app can attach to a Gateway."
}
}
private var gatewayModeGroup: some View {
SettingsCardGroup("Gateway") {
SettingsCardRow(
title: "OpenClaw runs",
subtitle: "Pick whether this app owns a local Gateway or attaches to another host.",
showsDivider: self.state.connectionMode == .unconfigured)
{
Picker("Gateway location", selection: self.$state.connectionMode) {
Text("Not configured").tag(AppState.ConnectionMode.unconfigured)
Text("Local (this Mac)").tag(AppState.ConnectionMode.local)
Text("Remote (another host)").tag(AppState.ConnectionMode.remote)
}
TailscaleIntegrationSection(
connectionMode: self.state.connectionMode,
isPaused: self.state.isPaused)
self.healthRow
.pickerStyle(.menu)
.labelsHidden()
.frame(width: 260, alignment: .trailing)
}
if self.state.connectionMode == .remote {
self.remoteCard
if self.state.connectionMode == .unconfigured {
SettingsCardRow(
title: "Setup needed",
subtitle: "Local is best for this Mac. Remote is best when the Gateway already runs on a Mac Studio or server.",
showsDivider: false)
{
Image(systemName: "exclamationmark.triangle.fill")
.foregroundStyle(.orange)
}
}
}
}
private var localGatewayGroup: some View {
VStack(alignment: .leading, spacing: 20) {
SettingsCardGroup("Local Gateway") {
if !self.isNixMode {
self.gatewayInstallerCard
}
self.healthRow
.padding(.horizontal, 14)
.padding(.vertical, 11)
}
TailscaleIntegrationSection(
connectionMode: self.state.connectionMode,
isPaused: self.state.isPaused)
}
}
private var remoteCard: some View {
VStack(alignment: .leading, spacing: 10) {
self.remoteTransportRow
VStack(alignment: .leading, spacing: 20) {
SettingsCardGroup("Remote Access") {
self.remoteTransportRow
if self.state.remoteTransport == .ssh {
self.remoteSshRow
} else {
self.remoteDirectRow
}
self.remoteTokenRow
}
SettingsCardGroup("Discovery & Status") {
self.remoteDiscoveryRow
self.remoteStatusRow
self.controlChannelRow
self.remoteTipRow
}
if self.state.remoteTransport == .ssh {
self.remoteSshRow
} else {
self.remoteDirectRow
self.remoteAdvancedGroup
}
self.remoteTokenRow
}
.transition(.opacity)
}
private var remoteDiscoveryRow: some View {
VStack(alignment: .leading, spacing: 8) {
Text("Nearby gateways")
.font(.callout.weight(.medium))
GatewayDiscoveryInlineList(
discovery: self.gatewayDiscovery,
currentTarget: self.state.remoteTarget,
@@ -159,92 +383,111 @@ struct GeneralSettings: View {
{ gateway in
self.applyDiscoveredGateway(gateway)
}
.padding(.leading, self.remoteLabelWidth + 10)
}
.padding(.horizontal, 14)
.padding(.vertical, 11)
.overlay(alignment: .bottom) {
Divider()
.padding(.leading, 14)
}
}
self.remoteStatusView
.padding(.leading, self.remoteLabelWidth + 10)
if self.state.remoteTransport == .ssh {
DisclosureGroup(isExpanded: self.$showRemoteAdvanced) {
VStack(alignment: .leading, spacing: 8) {
LabeledContent("Identity file") {
TextField("/Users/you/.ssh/id_ed25519", text: self.$state.remoteIdentity)
.textFieldStyle(.roundedBorder)
.frame(width: 280)
}
LabeledContent("Project root") {
TextField("/home/you/Projects/openclaw", text: self.$state.remoteProjectRoot)
.textFieldStyle(.roundedBorder)
.frame(width: 280)
}
LabeledContent("CLI path") {
TextField("/Applications/OpenClaw.app/.../openclaw", text: self.$state.remoteCliPath)
.textFieldStyle(.roundedBorder)
.frame(width: 280)
}
}
.padding(.top, 4)
} label: {
Text("Advanced")
.font(.callout.weight(.semibold))
}
}
// Diagnostics
VStack(alignment: .leading, spacing: 4) {
Text("Control channel")
.font(.caption.weight(.semibold))
if !self.isControlStatusDuplicate || ControlChannel.shared.lastPingMs != nil {
let status = self.isControlStatusDuplicate ? nil : self.controlStatusLine
let ping = ControlChannel.shared.lastPingMs.map { "Ping \(Int($0)) ms" }
let line = [status, ping].compactMap(\.self).joined(separator: " · ")
if !line.isEmpty {
Text(line)
.font(.caption)
.foregroundStyle(.secondary)
}
}
if let hb = HeartbeatStore.shared.lastEvent {
let ageText = age(from: Date(timeIntervalSince1970: hb.ts / 1000))
Text("Last heartbeat: \(hb.status) · \(ageText)")
.font(.caption)
.foregroundStyle(.secondary)
}
if let authLabel = ControlChannel.shared.authSourceLabel {
Text(authLabel)
.font(.caption)
.foregroundStyle(.secondary)
}
}
if self.state.remoteTransport == .ssh {
Text("Tip: enable Tailscale for stable remote access.")
.font(.footnote)
.foregroundStyle(.secondary)
.lineLimit(1)
} else {
Text("Tip: use Tailscale Serve so the gateway has a valid HTTPS cert.")
.font(.footnote)
.foregroundStyle(.secondary)
.lineLimit(2)
@ViewBuilder
private var remoteStatusRow: some View {
if self.remoteStatus != .idle {
SettingsCardRow(title: "Remote test") {
self.remoteStatusView
}
}
.transition(.opacity)
.onAppear { self.gatewayDiscovery.start() }
.onDisappear { self.gatewayDiscovery.stop() }
}
private var controlChannelRow: some View {
SettingsCardRow(title: "Control channel", subtitle: self.controlChannelSubtitle) {
Text(self.controlStatusLine)
.font(.caption.weight(.semibold))
.padding(.horizontal, 8)
.padding(.vertical, 4)
.background(self.connectionStatusTint.opacity(0.16), in: Capsule())
.foregroundStyle(self.connectionStatusTint)
}
}
private var controlChannelSubtitle: String? {
var parts: [String] = []
if let ping = ControlChannel.shared.lastPingMs {
parts.append("Ping \(Int(ping)) ms")
}
if let hb = HeartbeatStore.shared.lastEvent {
let ageText = age(from: Date(timeIntervalSince1970: hb.ts / 1000))
parts.append("Last heartbeat \(hb.status) · \(ageText)")
}
if let authLabel = ControlChannel.shared.authSourceLabel {
parts.append(authLabel)
}
return parts.isEmpty ? nil : parts.joined(separator: "\n")
}
private var remoteTipRow: some View {
SettingsCardRow(
title: "Recommended setup",
subtitle: self.state.remoteTransport == .ssh
? "Use Tailscale plus an SSH tunnel for stable private access."
: "Use Tailscale Serve so the gateway has a valid HTTPS certificate.",
showsDivider: false)
{
Image(systemName: "lightbulb.fill")
.foregroundStyle(.yellow)
}
}
private var remoteAdvancedGroup: some View {
SettingsCardGroup("Advanced") {
DisclosureGroup(isExpanded: self.$showRemoteAdvanced) {
VStack(alignment: .leading, spacing: 12) {
self.advancedTextField(
"Identity file",
placeholder: "/Users/you/.ssh/id_ed25519",
text: self.$state.remoteIdentity)
self.advancedTextField(
"Project root",
placeholder: "/home/you/Projects/openclaw",
text: self.$state.remoteProjectRoot)
self.advancedTextField(
"CLI path",
placeholder: "/Applications/OpenClaw.app/.../openclaw",
text: self.$state.remoteCliPath)
}
.padding(.top, 10)
} label: {
Text("SSH command details")
.font(.callout.weight(.medium))
}
.padding(.horizontal, 14)
.padding(.vertical, 11)
}
}
private func advancedTextField(_ title: String, placeholder: String, text: Binding<String>) -> some View {
VStack(alignment: .leading, spacing: 4) {
Text(title)
.font(.caption.weight(.semibold))
.foregroundStyle(.secondary)
TextField(placeholder, text: text)
.textFieldStyle(.roundedBorder)
}
}
private var remoteTransportRow: some View {
HStack(alignment: .center, spacing: 10) {
Text("Transport")
.font(.callout.weight(.semibold))
.frame(width: self.remoteLabelWidth, alignment: .leading)
SettingsCardRow(
title: "Transport",
subtitle: "SSH keeps the Gateway private; direct is best for HTTPS or Tailscale Serve.")
{
Picker("Transport", selection: self.$state.remoteTransport) {
Text("SSH tunnel").tag(AppState.RemoteTransport.ssh)
Text("Direct (ws/wss)").tag(AppState.RemoteTransport.direct)
}
.pickerStyle(.segmented)
.frame(maxWidth: 320)
.frame(width: 320)
}
}
@@ -253,59 +496,52 @@ struct GeneralSettings: View {
let validationMessage = CommandResolver.sshTargetValidationMessage(trimmedTarget)
let canTest = !trimmedTarget.isEmpty && validationMessage == nil
return VStack(alignment: .leading, spacing: 4) {
HStack(alignment: .center, spacing: 10) {
Text("SSH target")
.font(.callout.weight(.semibold))
.frame(width: self.remoteLabelWidth, alignment: .leading)
return VStack(alignment: .leading, spacing: 0) {
SettingsCardRow(title: "SSH target", subtitle: "User and host for the remote Gateway machine.") {
TextField("user@host[:22]", text: self.$state.remoteTarget)
.textFieldStyle(.roundedBorder)
.frame(maxWidth: .infinity)
.frame(width: 420)
self.remoteTestButton(disabled: !canTest)
}
if let validationMessage {
Text(validationMessage)
.font(.caption)
.foregroundStyle(.red)
.padding(.leading, self.remoteLabelWidth + 10)
.padding(.horizontal, 14)
.padding(.bottom, 10)
}
}
}
private var remoteDirectRow: some View {
VStack(alignment: .leading, spacing: 6) {
HStack(alignment: .center, spacing: 10) {
Text("Gateway")
.font(.callout.weight(.semibold))
.frame(width: self.remoteLabelWidth, alignment: .leading)
VStack(alignment: .leading, spacing: 0) {
SettingsCardRow(title: "Gateway URL", subtitle: "The WebSocket URL exposed by the remote Gateway.") {
TextField("wss://gateway.example.ts.net", text: self.$state.remoteUrl)
.textFieldStyle(.roundedBorder)
.frame(maxWidth: .infinity)
.frame(width: 420)
self.remoteTestButton(
disabled: self.state.remoteUrl.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
}
Text(
"Direct mode requires wss:// for remote hosts. ws:// is only allowed for localhost/127.0.0.1.")
"Use wss:// for public hosts. ws:// is allowed for localhost, LAN, .local, and Tailnet hosts.")
.font(.caption)
.foregroundStyle(.secondary)
.padding(.leading, self.remoteLabelWidth + 10)
.padding(.horizontal, 14)
.padding(.bottom, 10)
}
}
private var remoteTokenRow: some View {
VStack(alignment: .leading, spacing: 6) {
HStack(alignment: .center, spacing: 10) {
Text("Gateway token")
.font(.callout.weight(.semibold))
.frame(width: self.remoteLabelWidth, alignment: .leading)
VStack(alignment: .leading, spacing: 0) {
SettingsCardRow(
title: "Gateway token",
subtitle: "Used when the remote gateway requires token auth.",
showsDivider: false)
{
SecureField("remote gateway auth token (gateway.remote.token)", text: self.$state.remoteToken)
.textFieldStyle(.roundedBorder)
.frame(maxWidth: .infinity)
.frame(width: 360)
}
Text("Used when the remote gateway requires token auth.")
.font(.caption)
.foregroundStyle(.secondary)
.padding(.leading, self.remoteLabelWidth + 10)
if self.state.remoteTokenUnsupported {
Text(
"The current gateway.remote.token value is not plain text. "
@@ -313,7 +549,8 @@ struct GeneralSettings: View {
+ "enter a plaintext token here to replace it.")
.font(.caption)
.foregroundStyle(.orange)
.padding(.leading, self.remoteLabelWidth + 10)
.padding(.horizontal, 14)
.padding(.bottom, 10)
}
}
}
@@ -370,11 +607,6 @@ struct GeneralSettings: View {
}
}
private var isControlStatusDuplicate: Bool {
guard case let .failed(message) = self.remoteStatus else { return false }
return message == self.controlStatusLine
}
private var gatewayInstallerCard: some View {
VStack(alignment: .leading, spacing: 8) {
HStack(spacing: 10) {

View File

@@ -3,13 +3,15 @@ import SwiftUI
struct InstancesSettings: View {
var store: InstancesStore
let isActive: Bool
init(store: InstancesStore = .shared) {
init(store: InstancesStore = .shared, isActive: Bool = true) {
self.store = store
self.isActive = isActive
}
var body: some View {
VStack(alignment: .leading, spacing: 12) {
VStack(alignment: .leading, spacing: 16) {
self.header
if let err = store.lastError {
Text("Error: \(err)")
@@ -29,20 +31,37 @@ struct InstancesSettings: View {
}
Spacer()
}
.onAppear { self.store.start() }
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
.padding(.leading, 18)
.padding(.trailing, SettingsLayout.scrollbarGutter)
.onAppear { self.updateActiveWork(active: self.isActive) }
.onChange(of: self.isActive) { _, active in
self.updateActiveWork(active: active)
}
.onDisappear { self.store.stop() }
}
private func updateActiveWork(active: Bool) {
if active {
self.store.start()
} else {
self.store.stop()
}
}
private var header: some View {
HStack {
VStack(alignment: .leading, spacing: 4) {
HStack(alignment: .top, spacing: 16) {
VStack(alignment: .leading, spacing: 5) {
Text("Connected Instances")
.font(.headline)
.font(.title3.weight(.semibold))
Text("Latest presence beacons from OpenClaw nodes. Updated periodically.")
.font(.footnote)
.font(.callout)
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
}
Spacer()
Spacer(minLength: 16)
SettingsRefreshButton(isLoading: self.store.isLoading) {
Task { await self.store.refresh() }
}

View File

@@ -91,8 +91,11 @@ struct OpenClawApp: App {
}
}
private func applyStatusItemAppearance(paused: Bool, sleeping: Bool) {
self.statusItem?.button?.appearsDisabled = paused || sleeping
private func applyStatusItemAppearance(paused _: Bool, sleeping _: Bool) {
// Keep the status item actionable even when the Gateway is paused or disconnected.
// The SwiftUI label already renders those states; AppKit's disabled appearance can
// leak into menu item validation and grey out app-level commands like Settings.
self.statusItem?.button?.appearsDisabled = false
}
private static func applyAttachOnlyOverrideIfNeeded() {
@@ -143,7 +146,7 @@ struct OpenClawApp: App {
handler.translatesAutoresizingMaskIntoConstraints = false
handler.onLeftClick = { [self] in
HoverHUDController.shared.dismiss(reason: "statusItemClick")
self.toggleWebChatPanel()
self.openDashboardWindow()
}
handler.onRightClick = { [self] in
HoverHUDController.shared.dismiss(reason: "statusItemRightClick")
@@ -167,14 +170,21 @@ struct OpenClawApp: App {
}
@MainActor
private func toggleWebChatPanel() {
private func openDashboardWindow() {
HoverHUDController.shared.setSuppressed(true)
self.isMenuPresented = false
if DashboardManager.shared.showConfiguredWindowIfPossible() {
return
}
Task { @MainActor in
let sessionKey = await WebChatManager.shared.preferredSessionKey()
WebChatManager.shared.togglePanel(
sessionKey: sessionKey,
anchorProvider: { [self] in self.statusButtonScreenFrame() })
if DashboardManager.shared.showConfiguredWindowIfPossible() {
return
}
do {
try await DashboardManager.shared.show()
} catch {
DashboardManager.shared.showFailure(error)
}
}
}
@@ -283,6 +293,19 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
WebChatManager.shared.show(sessionKey: sessionKey)
}
}
if CommandLine.arguments.contains("--dashboard") {
self.webChatAutoLogger.info("Auto-opening dashboard via CLI flag")
Task { @MainActor in
if DashboardManager.shared.showConfiguredWindowIfPossible() {
return
}
do {
try await DashboardManager.shared.show()
} catch {
DashboardManager.shared.showFailure(error)
}
}
}
}
func applicationWillTerminate(_ notification: Notification) {
@@ -294,6 +317,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
MacNodeModeCoordinator.shared.stop()
TerminationSignalWatcher.shared.stop()
VoiceWakeGlobalSettingsSync.shared.stop()
DashboardManager.shared.close()
WebChatManager.shared.close()
WebChatManager.shared.resetTunnels()
Task { await RemoteTunnelManager.shared.stopAll() }
@@ -303,6 +327,10 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
@MainActor
private func scheduleFirstRunOnboardingIfNeeded() {
if AppStateStore.shared.connectionMode != .unconfigured {
OnboardingController.markComplete()
return
}
let seenVersion = UserDefaults.standard.integer(forKey: onboardingVersionKey)
let shouldShow = seenVersion < currentOnboardingVersion || !AppStateStore.shared.onboardingSeen
guard shouldShow else { return }

View File

@@ -111,11 +111,7 @@ struct MenuContent: View {
self.voiceWakeMicMenu
}
Divider()
Button {
Task { @MainActor in
await self.openDashboard()
}
} label: {
Link(destination: URL(string: "openclaw://dashboard")!) {
Label("Open Dashboard", systemImage: "gauge")
}
Button {
@@ -342,20 +338,6 @@ struct MenuContent: View {
}
}
@MainActor
private func openDashboard() async {
do {
let config = try await GatewayEndpointStore.shared.requireConfig()
let url = try GatewayEndpointStore.dashboardURL(for: config, mode: self.state.connectionMode)
NSWorkspace.shared.open(url)
} catch {
let alert = NSAlert()
alert.messageText = "Dashboard unavailable"
alert.informativeText = error.localizedDescription
alert.runModal()
}
}
private var macNodeStatus: (label: String, color: Color)? {
guard self.state.connectionMode != .unconfigured else { return nil }
guard case .connected = self.controlChannel.state else { return nil }

View File

@@ -37,8 +37,10 @@ struct MenuHeaderCard<Content: View>: View {
Text(statusText)
.font(.caption)
.foregroundStyle(.secondary)
.lineLimit(1)
.multilineTextAlignment(.leading)
.lineLimit(5)
.truncationMode(.tail)
.fixedSize(horizontal: false, vertical: true)
}
self.content
}

View File

@@ -65,28 +65,28 @@ final class MenuSessionsInjector: NSObject, NSMenuDelegate {
self.inject(into: menu)
self.injectNodes(into: menu)
// Refresh in background for the next open; keep width stable while open.
// Refresh in the background for the next open. Rebuilding custom menu
// rows while AppKit is tracking the menu causes visible flicker.
self.loadTask?.cancel()
let forceRefresh = self.cachedSnapshot == nil || self.cachedErrorText != nil
self.loadTask = Task { [weak self] in
let shouldRepaintAfterRefresh = self.cachedSnapshot == nil || self.cachedErrorText != nil
self.loadTask = Task { [weak self, weak menu] in
guard let self else { return }
let forceRefresh = shouldRepaintAfterRefresh
await self.refreshCache(force: forceRefresh)
await self.refreshUsageCache(force: forceRefresh)
await self.refreshCostUsageCache(force: forceRefresh)
await MainActor.run {
guard self.isMenuOpen else { return }
self.inject(into: menu)
self.injectNodes(into: menu)
if shouldRepaintAfterRefresh {
await self.repaintOpenMenu(menu)
}
}
self.nodesLoadTask?.cancel()
self.nodesLoadTask = Task { [weak self] in
let shouldRepaintNodesAfterRefresh = self.shouldRepaintNodesAfterRefresh()
self.nodesLoadTask = Task { [weak self, weak menu] in
guard let self else { return }
await self.nodesStore.refresh()
await MainActor.run {
guard self.isMenuOpen else { return }
self.injectNodes(into: menu)
if !shouldRepaintAfterRefresh, shouldRepaintNodesAfterRefresh {
await self.repaintOpenMenuNodes(menu)
}
}
}
@@ -95,8 +95,6 @@ final class MenuSessionsInjector: NSObject, NSMenuDelegate {
self.originalDelegate?.menuDidClose?(menu)
self.isMenuOpen = false
self.menuOpenWidth = nil
self.loadTask?.cancel()
self.nodesLoadTask?.cancel()
self.cancelPreviewTasks()
}
@@ -122,25 +120,18 @@ final class MenuSessionsInjector: NSObject, NSMenuDelegate {
guard self.isMenuOpen, let menu = self.statusItem?.menu else { return }
self.loadTask?.cancel()
self.loadTask = Task { [weak self, weak menu] in
guard let self, let menu else { return }
guard let self else { return }
await self.refreshCache(force: true)
await self.refreshUsageCache(force: true)
await self.refreshCostUsageCache(force: true)
await MainActor.run {
guard self.isMenuOpen else { return }
self.inject(into: menu)
self.injectNodes(into: menu)
}
await self.repaintOpenMenu(menu)
}
self.nodesLoadTask?.cancel()
self.nodesLoadTask = Task { [weak self, weak menu] in
guard let self, let menu else { return }
guard let self else { return }
await self.nodesStore.refresh()
await MainActor.run {
guard self.isMenuOpen else { return }
self.injectNodes(into: menu)
}
await self.repaintOpenMenuNodes(menu)
}
}
@@ -278,6 +269,28 @@ extension MenuSessionsInjector {
_ = cursor
}
private func repaintOpenMenu(_ menu: NSMenu?) async {
await MainActor.run {
guard self.isMenuOpen, let menu else { return }
self.inject(into: menu)
self.injectNodes(into: menu)
}
}
private func repaintOpenMenuNodes(_ menu: NSMenu?) async {
await MainActor.run {
guard self.isMenuOpen, let menu else { return }
self.injectNodes(into: menu)
}
}
private func shouldRepaintNodesAfterRefresh() -> Bool {
guard self.isControlChannelConnected else { return false }
return self.sortedNodeEntries().isEmpty
|| self.nodesStore.lastError?.nonEmpty != nil
|| self.nodesStore.statusMessage?.nonEmpty != nil
}
private func buildContextSubmenu(
width: CGFloat,
isConnected: Bool,
@@ -374,7 +387,7 @@ extension MenuSessionsInjector {
return self.cachedErrorText ?? "Loading…"
}
return self.controlChannelStatusText(for: channelState)
return Self.menuStatusText(self.controlChannelStatusText(for: channelState))
}
private func activeRows(from snapshot: SessionStoreSnapshot) -> [SessionRow] {
@@ -527,7 +540,7 @@ extension MenuSessionsInjector {
case .connecting:
"Connecting…"
case let .degraded(message):
message.nonEmpty ?? "Gateway disconnected"
Self.menuStatusText(message.nonEmpty ?? "Gateway disconnected")
case .disconnected:
"Gateway disconnected"
}
@@ -684,7 +697,7 @@ extension MenuSessionsInjector {
self.previewTasks.removeAll()
}
private func makeMessageItem(text: String, symbolName: String, width: CGFloat, maxLines: Int? = 2) -> NSMenuItem {
private func makeMessageItem(text: String, symbolName: String, width: CGFloat, maxLines: Int? = 3) -> NSMenuItem {
let view = AnyView(
HStack(alignment: .top, spacing: 8) {
Image(systemName: symbolName)
@@ -810,6 +823,19 @@ extension MenuSessionsInjector {
}
return "Sessions unavailable"
}
private static func menuStatusText(_ text: String) -> String {
let lines = text
.trimmingCharacters(in: .whitespacesAndNewlines)
.split(whereSeparator: \.isNewline)
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
.filter { !$0.isEmpty }
let singleLine = (lines.isEmpty ? text : lines.joined(separator: " "))
.trimmingCharacters(in: .whitespacesAndNewlines)
guard singleLine.count > 180 else { return singleLine }
return "\(singleLine.prefix(177))"
}
}
extension MenuSessionsInjector {
@@ -1283,6 +1309,14 @@ extension MenuSessionsInjector {
self.inject(into: menu)
}
func testingControlChannelStatusText(for state: ControlChannel.ConnectionState) -> String {
self.controlChannelStatusText(for: state)
}
func testingMenuStatusText(_ text: String) -> String {
Self.menuStatusText(text)
}
func testingFindInsertIndex(in menu: NSMenu) -> Int? {
self.findInsertIndex(in: menu)
}

View File

@@ -280,6 +280,7 @@ final class NodePairingApprovalPrompter {
requestId: req.requestId,
messageText: "Allow node to connect?",
informativeText: Self.describe(req),
buttonTitles: PairingAlertSupport.ButtonTitles(approve: "Approve Node"),
state: self.alertState,
onResponse: self.handleAlertResponse)
}
@@ -307,11 +308,11 @@ final class NodePairingApprovalPrompter {
switch response {
case .alertFirstButtonReturn:
// Later: leave as pending (CLI can approve/reject). Request will expire on the gateway TTL.
return
case .alertSecondButtonReturn:
_ = await self.approve(requestId: request.requestId)
await self.notify(resolution: .approved, request: request, via: "local")
case .alertSecondButtonReturn:
// Later: leave as pending (CLI can approve/reject). Request will expire on the gateway TTL.
return
case .alertThirdButtonReturn:
await self.reject(requestId: request.requestId)
await self.notify(resolution: .rejected, request: request, via: "local")

View File

@@ -21,12 +21,16 @@ final class OnboardingController {
static let shared = OnboardingController()
private var window: NSWindow?
static func markComplete() {
UserDefaults.standard.set(true, forKey: onboardingSeenKey)
UserDefaults.standard.set(currentOnboardingVersion, forKey: onboardingVersionKey)
AppStateStore.shared.onboardingSeen = true
}
func show() {
if ProcessInfo.processInfo.isNixMode {
// Nix mode is fully declarative; onboarding would suggest interactive setup that doesn't apply.
UserDefaults.standard.set(true, forKey: "openclaw.onboardingSeen")
UserDefaults.standard.set(currentOnboardingVersion, forKey: onboardingVersionKey)
AppStateStore.shared.onboardingSeen = true
Self.markComplete()
return
}
if let window {

View File

@@ -54,8 +54,7 @@ extension OnboardingView {
}
func finish() {
UserDefaults.standard.set(true, forKey: "openclaw.onboardingSeen")
UserDefaults.standard.set(currentOnboardingVersion, forKey: onboardingVersionKey)
OnboardingController.markComplete()
OnboardingController.shared.close()
}

View File

@@ -321,7 +321,7 @@ extension OnboardingView {
return "Select a nearby gateway or open Advanced to enter a gateway URL."
}
if GatewayRemoteConfig.normalizeGatewayUrl(trimmedUrl) == nil {
return "Gateway URL must use wss:// for remote hosts (ws:// only for localhost)."
return "Gateway URL must use wss:// for public hosts; ws:// is allowed for localhost, LAN, or Tailnet hosts."
}
return nil
case .ssh:

View File

@@ -301,6 +301,16 @@ enum OpenClawConfigFile {
}
}
static func setRemoteGatewayTransport(_ value: String) {
let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return }
self.updateGatewayDict { gateway in
var remote = gateway["remote"] as? [String: Any] ?? [:]
remote["transport"] = trimmed
gateway["remote"] = remote
}
}
static func clearRemoteGatewayUrl() {
self.updateGatewayDict { gateway in
guard var remote = gateway["remote"] as? [String: Any] else { return }

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